Compare commits
19 commits
d9de502894
...
6bba18e208
Author | SHA1 | Date | |
---|---|---|---|
6bba18e208 | |||
![]() |
36faaf0b97 | ||
![]() |
e56893fd64 | ||
![]() |
c553e2077a | ||
![]() |
76ed927839 | ||
![]() |
08de84a889 | ||
![]() |
52b81cc115 | ||
![]() |
de08c6c5b1 | ||
![]() |
ee566bd2a3 | ||
![]() |
9d8aec1aa6 | ||
![]() |
9dc8fe77bc | ||
![]() |
409851a64c | ||
![]() |
cd1b556fb1 | ||
![]() |
467fc935f0 | ||
![]() |
a98f9c698b | ||
![]() |
6431e7145d | ||
![]() |
47348190ab | ||
![]() |
cbf99fad18 | ||
![]() |
c06d5f1f30 |
11 changed files with 200 additions and 27 deletions
19
.direnv/bin/nix-direnv-reload
Executable file
19
.direnv/bin/nix-direnv-reload
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
if [[ ! -d "/home/amy/code/public/oss/frontend" ]]; then
|
||||||
|
echo "Cannot find source directory; Did you move it?"
|
||||||
|
echo "(Looking for "/home/amy/code/public/oss/frontend")"
|
||||||
|
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rebuild the cache forcefully
|
||||||
|
_nix_direnv_force_reload=1 direnv exec "/home/amy/code/public/oss/frontend" true
|
||||||
|
|
||||||
|
# Update the mtime for .envrc.
|
||||||
|
# This will cause direnv to reload again - but without re-building.
|
||||||
|
touch "/home/amy/code/public/oss/frontend/.envrc"
|
||||||
|
|
||||||
|
# Also update the timestamp of whatever profile_rc we have.
|
||||||
|
# This makes sure that we know we are up to date.
|
||||||
|
touch -r "/home/amy/code/public/oss/frontend/.envrc" "/home/amy/code/public/oss/frontend/.direnv"/*.rc
|
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
|
@ -49,4 +49,4 @@ While maintaining Donetick's commitment to open source, this hosted option will
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. I might consider changing it later to something else
|
This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for more details. I might consider changing it later to something else
|
||||||
|
|
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1743550720,
|
||||||
|
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1743583204,
|
||||||
|
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1743296961,
|
||||||
|
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
23
flake.nix
Normal file
23
flake.nix
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
description = "Description for the project";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs@{ flake-parts, ... }:
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
|
||||||
|
perSystem = { config, self', inputs', pkgs, system, ... }: {
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
nodePackages.npm
|
||||||
|
nodejs
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
flake = {
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -179,9 +179,9 @@ export const notInCompletionWindow = chore => {
|
||||||
export const ChoreFilters = userProfile => ({
|
export const ChoreFilters = userProfile => ({
|
||||||
anyone: () => true,
|
anyone: () => true,
|
||||||
assigned_to_me: chore => {
|
assigned_to_me: chore => {
|
||||||
return chore.assignedTo && chore.assignedTo === userProfile.id
|
return chore.assignedTo && chore.assignedTo === userProfile?.id
|
||||||
},
|
},
|
||||||
assigned_to_others: chore => {
|
assigned_to_others: chore => {
|
||||||
return chore.assignedTo && chore.assignedTo !== userProfile.id
|
return chore.assignedTo && chore.assignedTo !== userProfile?.id
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -115,7 +115,7 @@ const LoginView = () => {
|
||||||
Navigate('/forgot-password')
|
Navigate('/forgot-password')
|
||||||
}
|
}
|
||||||
const generateRandomState = () => {
|
const generateRandomState = () => {
|
||||||
const randomState = Math.random().toString(36).substring(7)
|
const randomState = Math.random().toString(32).substring(5)
|
||||||
localStorage.setItem('authState', randomState)
|
localStorage.setItem('authState', randomState)
|
||||||
|
|
||||||
return randomState
|
return randomState
|
||||||
|
|
|
@ -74,7 +74,7 @@ const MyChores = () => {
|
||||||
JSON.parse(localStorage.getItem('openChoreSections')) || {},
|
JSON.parse(localStorage.getItem('openChoreSections')) || {},
|
||||||
)
|
)
|
||||||
const [selectedChoreFilter, setSelectedChoreFilter] = useState(
|
const [selectedChoreFilter, setSelectedChoreFilter] = useState(
|
||||||
JSON.parse(localStorage.getItem('selectedChoreFilter')) || 'anyone',
|
localStorage.getItem('selectedChoreFilter') || 'anyone',
|
||||||
)
|
)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [performers, setPerformers] = useState([])
|
const [performers, setPerformers] = useState([])
|
||||||
|
@ -98,13 +98,12 @@ const MyChores = () => {
|
||||||
throw new Error(userProfileResponse.statusText)
|
throw new Error(userProfileResponse.statusText)
|
||||||
}
|
}
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
userProfileResponse.json(),
|
||||||
choresResponse.json(),
|
choresResponse.json(),
|
||||||
usersResponse.json(),
|
usersResponse.json(),
|
||||||
userProfileResponse.json(),
|
|
||||||
]).then(data => {
|
]).then(data => {
|
||||||
const [choresData, usersData, userProfileData] = data
|
const [userProfileData, choresData, usersData] = data
|
||||||
setUserProfile(userProfileData.res)
|
setUserProfile(userProfileData.res)
|
||||||
choresData.res.sort(ChoreSorter)
|
|
||||||
setChores(choresData.res)
|
setChores(choresData.res)
|
||||||
setFilteredChores(choresData.res)
|
setFilteredChores(choresData.res)
|
||||||
setPerformers(usersData.res)
|
setPerformers(usersData.res)
|
||||||
|
@ -139,7 +138,11 @@ const MyChores = () => {
|
||||||
const sortedChores = choresData.res.sort(ChoreSorter)
|
const sortedChores = choresData.res.sort(ChoreSorter)
|
||||||
setChores(sortedChores)
|
setChores(sortedChores)
|
||||||
setFilteredChores(sortedChores)
|
setFilteredChores(sortedChores)
|
||||||
const sections = ChoresGrouper(selectedChoreSection, sortedChores)
|
const sections = ChoresGrouper(
|
||||||
|
selectedChoreSection,
|
||||||
|
sortedChores,
|
||||||
|
ChoreFilters(userProfile)[selectedChoreFilter],
|
||||||
|
)
|
||||||
setChoreSections(sections)
|
setChoreSections(sections)
|
||||||
if (localStorage.getItem('openChoreSections') === null) {
|
if (localStorage.getItem('openChoreSections') === null) {
|
||||||
setSelectedChoreSectionWithCache(selectedChoreSection)
|
setSelectedChoreSectionWithCache(selectedChoreSection)
|
||||||
|
@ -178,7 +181,7 @@ const MyChores = () => {
|
||||||
}
|
}
|
||||||
const setSelectedChoreFilterWithCache = value => {
|
const setSelectedChoreFilterWithCache = value => {
|
||||||
setSelectedChoreFilter(value)
|
setSelectedChoreFilter(value)
|
||||||
localStorage.setItem('selectedChoreFilter', JSON.stringify(value))
|
localStorage.setItem('selectedChoreFilter', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateChores = newChore => {
|
const updateChores = newChore => {
|
||||||
|
@ -186,7 +189,13 @@ const MyChores = () => {
|
||||||
newChores.push(newChore)
|
newChores.push(newChore)
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newChores)
|
setFilteredChores(newChores)
|
||||||
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
setChoreSections(
|
||||||
|
ChoresGrouper(
|
||||||
|
selectedChoreSection,
|
||||||
|
newChores,
|
||||||
|
ChoreFilters(userProfile)[selectedChoreFilter],
|
||||||
|
),
|
||||||
|
)
|
||||||
setSearchFilter('All')
|
setSearchFilter('All')
|
||||||
}
|
}
|
||||||
const handleMenuOutsideClick = event => {
|
const handleMenuOutsideClick = event => {
|
||||||
|
@ -262,7 +271,13 @@ const MyChores = () => {
|
||||||
}
|
}
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newFilteredChores)
|
setFilteredChores(newFilteredChores)
|
||||||
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
setChoreSections(
|
||||||
|
ChoresGrouper(
|
||||||
|
selectedChoreSection,
|
||||||
|
newChores,
|
||||||
|
ChoreFilters(userProfile)[selectedChoreFilter],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
|
@ -293,7 +308,13 @@ const MyChores = () => {
|
||||||
)
|
)
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newFilteredChores)
|
setFilteredChores(newFilteredChores)
|
||||||
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
setChoreSections(
|
||||||
|
ChoresGrouper(
|
||||||
|
selectedChoreSection,
|
||||||
|
newChores,
|
||||||
|
ChoreFilters(userProfile)[selectedChoreFilter],
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchOptions = {
|
const searchOptions = {
|
||||||
|
@ -449,7 +470,11 @@ const MyChores = () => {
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
onItemSelect={selected => {
|
onItemSelect={selected => {
|
||||||
const section = ChoresGrouper(selected.value, chores)
|
const section = ChoresGrouper(
|
||||||
|
selected.value,
|
||||||
|
chores,
|
||||||
|
ChoreFilters(userProfile)[selectedChoreFilter],
|
||||||
|
)
|
||||||
setChoreSections(section)
|
setChoreSections(section)
|
||||||
setSelectedChoreSectionWithCache(selected.value)
|
setSelectedChoreSectionWithCache(selected.value)
|
||||||
setOpenChoreSectionsWithCache(
|
setOpenChoreSectionsWithCache(
|
||||||
|
|
|
@ -98,6 +98,10 @@ const NotificationSetting = () => {
|
||||||
const [chatID, setChatID] = useState(
|
const [chatID, setChatID] = useState(
|
||||||
userProfile?.notification_target?.target_id,
|
userProfile?.notification_target?.target_id,
|
||||||
)
|
)
|
||||||
|
const [webhookURL, setWebhookURL] = useState(
|
||||||
|
userProfile?.notification_target?.target_id,
|
||||||
|
)
|
||||||
|
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const SaveValidation = () => {
|
const SaveValidation = () => {
|
||||||
switch (notificationTarget) {
|
switch (notificationTarget) {
|
||||||
|
@ -116,6 +120,12 @@ const NotificationSetting = () => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case '4':
|
||||||
|
if (webhookURL === '') {
|
||||||
|
setError('Webhook URL is required')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -126,7 +136,8 @@ const NotificationSetting = () => {
|
||||||
if (!SaveValidation()) return
|
if (!SaveValidation()) return
|
||||||
|
|
||||||
UpdateNotificationTarget({
|
UpdateNotificationTarget({
|
||||||
target: chatID,
|
// 4 = Discord
|
||||||
|
target: notificationTarget === '4' ? webhookURL : chatID,
|
||||||
type: Number(notificationTarget),
|
type: Number(notificationTarget),
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
if (resp.status != 200) {
|
if (resp.status != 200) {
|
||||||
|
@ -134,13 +145,26 @@ const NotificationSetting = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserProfile({
|
// Discord
|
||||||
...userProfile,
|
if (notificationTarget === '4') {
|
||||||
notification_target: {
|
setUserProfile({
|
||||||
target: chatID,
|
...userProfile,
|
||||||
type: Number(notificationTarget),
|
notification_target: {
|
||||||
},
|
target: webhookURL,
|
||||||
})
|
type: Number(notificationTarget),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Others (Telegram)
|
||||||
|
setUserProfile({
|
||||||
|
...userProfile,
|
||||||
|
notification_target: {
|
||||||
|
target: chatID,
|
||||||
|
type: Number(notificationTarget),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
alert('Notification target updated')
|
alert('Notification target updated')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -323,7 +347,7 @@ const NotificationSetting = () => {
|
||||||
<Typography level='h3'>Custom Notification</Typography>
|
<Typography level='h3'>Custom Notification</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Typography level='body-md'>
|
<Typography level='body-md'>
|
||||||
Notificaiton through other platform like Telegram or Pushover
|
Notification through other platform like Telegram or Pushover
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<FormControl orientation='horizontal'>
|
<FormControl orientation='horizontal'>
|
||||||
|
@ -380,6 +404,7 @@ const NotificationSetting = () => {
|
||||||
<Option value='0'>None</Option>
|
<Option value='0'>None</Option>
|
||||||
<Option value='1'>Telegram</Option>
|
<Option value='1'>Telegram</Option>
|
||||||
<Option value='2'>Pushover</Option>
|
<Option value='2'>Pushover</Option>
|
||||||
|
<Option value='4'>Discord</Option>
|
||||||
<Option value='3'>Webhooks</Option>
|
<Option value='3'>Webhooks</Option>
|
||||||
</Select>
|
</Select>
|
||||||
{notificationTarget === '1' && (
|
{notificationTarget === '1' && (
|
||||||
|
@ -438,6 +463,19 @@ const NotificationSetting = () => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{notificationTarget === '4' && (
|
||||||
|
<>
|
||||||
|
<Typography level='body-sm'>Webhook URL</Typography>
|
||||||
|
<Input
|
||||||
|
value={webhookURL}
|
||||||
|
onChange={e => setWebhookURL(e.target.value)}
|
||||||
|
placeholder='Webhook URL'
|
||||||
|
sx={{
|
||||||
|
width: '200px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<Typography color='warning' level='body-sm'>
|
<Typography color='warning' level='body-sm'>
|
||||||
{error}
|
{error}
|
||||||
|
|
|
@ -430,12 +430,18 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
||||||
assignedTo: userProfile.id,
|
assignedTo: userProfile.id,
|
||||||
assignStrategy: 'random',
|
assignStrategy: 'random',
|
||||||
isRolling: false,
|
isRolling: false,
|
||||||
notification: false,
|
notification: true,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
labelsV2: [],
|
labelsV2: [],
|
||||||
priority: priority || 0,
|
priority: priority || 0,
|
||||||
status: 0,
|
status: 0,
|
||||||
frequencyType: 'once',
|
frequencyType: 'once',
|
||||||
|
frequencyMetadata: {},
|
||||||
|
notificationMetadata: {
|
||||||
|
dueDate: true,
|
||||||
|
predue: true,
|
||||||
|
nagging: true
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frequency) {
|
if (frequency) {
|
||||||
|
|
|
@ -105,12 +105,12 @@ const NavBar = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='flex gap-2 p-2'>
|
<nav className='flex gap-2 p-3'>
|
||||||
<IconButton size='sm' variant='plain' onClick={() => setDrawerOpen(true)}>
|
<IconButton size='md' variant='plain' onClick={() => setDrawerOpen(true)}>
|
||||||
<MenuRounded />
|
<MenuRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Box
|
<Box
|
||||||
className='flex items-center gap-1'
|
className='flex items-center gap-2'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/my/chores')
|
navigate('/my/chores')
|
||||||
}}
|
}}
|
||||||
|
|
Loading…
Add table
Reference in a new issue