├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── locales ├── en.yml └── fr.yml ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── public ├── potato.png ├── pwa-192x192.png ├── pwa-512x512.png └── robots.txt ├── src ├── App.vue ├── assets │ ├── potatoCry.png │ ├── potatoDetect.png │ ├── potatoHappy.png │ ├── potatoLost.png │ ├── potatoLurk.png │ ├── potatoNap.png │ ├── potatoNote.png │ ├── potatoNotif.png │ ├── potatoParty.png │ └── potatoShy.png ├── auto-imports.d.ts ├── components.d.ts ├── components │ ├── InfoPanel.vue │ ├── LanguageSelector.vue │ ├── NotificationsWarning.vue │ ├── OptionControls.vue │ ├── PotatoProgress.vue │ ├── PotatoSettings.vue │ ├── PotatoTimer.vue │ ├── ResetButton.vue │ ├── TaskList.vue │ └── ThemeSelector.vue ├── composables │ ├── useNotifier.ts │ ├── usePotato.ts │ ├── usePotatoStorage.ts │ └── useTasks.ts ├── layouts │ └── default.vue ├── logic │ ├── dark.ts │ └── index.ts ├── main.ts ├── pages │ └── index.vue └── shims.d.ts ├── tsconfig.json └── vite.config.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: prazdevs 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .vite-ssg-dist 4 | .vite-ssg-temp 5 | *.local 6 | codecov.exe 7 | coverage 8 | cypress/screenshots 9 | cypress/videos 10 | dist 11 | dist-ssr 12 | node_modules 13 | # intellij stuff 14 | .idea/ 15 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sacha Bouillez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Artwork by Robynn Frauhn and Hels 7 |

8 | 9 |

Potato Timer

10 | 11 |

A pomodoro timer, but with potatoes. Because potatoes are cool.

12 | 13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 |

Automagically hosted on Netlify.

23 | 24 |

25 | 26 | 27 | 28 |

29 | 30 | ## ✨ Features 31 | 32 | - Persistent timers through sessions/tabs. 33 | - Persistent task list. 34 | - Configurable timers. 35 | - Desktop notifications. 36 | - PWA support. 37 | 38 | ## 🚀 Usage 39 | 40 | Available at [this url](https://potatotimer.app). Tutorial available in the app. 41 | 42 | ## ❓ What is this thing? 43 | 44 | This app is a sandbox I made to use Vue3 and Vite, alongside interesting packages such as VueUse. It features lots of different stuff such as: 45 | - Vue 3 w/ `script setup` syntax. 46 | - Vite with plugins (`layouts`, `pages`, `components`, `i18n`... 47 | - Fully typed with TypeScript. 48 | - Full PWA support. 49 | - Cypress tests with Gherkin syntax (using Cucumber.js). 50 | - Accessibility features such as focus traps and focus management. 51 | 52 | ## 🤝 Contributing 53 | 54 | Any contribution to the project is welome. 55 | Run into a problem? Open an [issue](https://github.com/prazdevs/potato-timer/issues/new/choose). 56 | Want to add some feature? PRs are welcome! 57 | 58 | 59 | ## 👤 About the author 60 | 61 | Feel free to contact me: 62 | 63 | - 64 | - 65 | 66 | ## 📝 Licence 67 | 68 | Copyright © 2021 [Sacha Bouillez](https://github.com/prazdevs).
69 | This project is under [MIT](https://github.com/prazdevs/potato-timer/blob/main/LICENCE) license.
70 | _The potato images used belong to [Robynn Frauhn](https://twitter.com/RFrauhn) and [Hels](https://twitter.com/hels_draws)._ 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | common: 2 | close: Close the modal 3 | incompatibility: Incompatibility 4 | pause: Pause 5 | reset: Reset 6 | resume: Resume 7 | warning: Warning 8 | footer: 9 | credits: Potato credits to {robynn} and {hels} 10 | hels: HelS 11 | made: Made with ❤️ by {praz} 12 | praz: PraZ 13 | robynn: Robynn Frauhn 14 | info: 15 | developer: 16 | contact: contact me 17 | content: >- 18 | This app is my gift to a lovely and wholesome community that helped me a 19 | lot. It is also a sandbox that allowed me to experiment with technologies 20 | I like. I tried to make it as accessible as possible. However there might 21 | be bugs, and if you find any, you can {contact}. If you are a developer, 22 | you can also {contribute}. Finally, if you want to contribute in another 23 | way, you can always {kofi}. 24 | contribute: contribute 25 | kofi: offer me a coffee 26 | title: A word from the developer 27 | origin: 28 | content: >- 29 | A lot of small potatoes from the marvelous community of Robynn Frauhn were 30 | used to the Pomodoro technique to help them work. As in Robynn's oasis, 31 | potatoes reign, they decided to replace tomates in their technique. This 32 | is how was born the Potato technique and its Potato Timer. 33 | title: The origin of the Potato Timer 34 | technique: 35 | content: >- 36 | The Pomodoro technique is a time management technique. It consists of 37 | defining periods of work, separated by smaller periods of pause. Before 38 | starting, several tasks should be planned, so they can be worked on during 39 | work time. It can be split in 5 steps: 40 | steps: 41 | - Defining the task to work on. 42 | - Setup a work timer (25 minutes). 43 | - Work on the task for 25 minutes. 44 | - Take a 5 minute break. 45 | - Start over with a 15-30 minute pause on the 4th cycle. 46 | title: The Pomodoro technique 47 | title: What is Potato Timer ? 48 | not-found: Not found 49 | notification: 50 | api-unavailable: >- 51 | Your device or browser is not compatible with notifications. You will not 52 | receive any when it's time to take a break or get back to work. 53 | permission-warning: >- 54 | You didn't allow the app to send you notifications. You may miss the moment 55 | to take a break ! 56 | request-permission: Allow notifications 57 | options: 58 | change-language: Change language — currently {current} 59 | change-theme: Change theme — currently {current} 60 | info: About the app 61 | title: Options 62 | reset: 63 | cancel: Cancel 64 | confirm: Reset 65 | message: There is a potato running, are you sure you want to reset? 66 | settings: 67 | cancel: Cancel 68 | long-pause: Final pause duration (minutes) 69 | pause-time: Pause potato duration (minutes) 70 | reset: Restore default durations 71 | save: Save 72 | title: Settings 73 | work-time: Work potato duration (minutes) 74 | steps: 75 | done: We're done ! 76 | pause: Time to chill... 77 | ready: Ready to start ? 78 | work: Let's get to work ! 79 | tasks: 80 | add: Add task 81 | delete: Delete task "{task}" 82 | empty: No task to work on. 83 | timer: Potato progress 84 | theme: 85 | dark: Dark 86 | light: Light 87 | -------------------------------------------------------------------------------- /locales/fr.yml: -------------------------------------------------------------------------------- 1 | common: 2 | close: Fermer la modale 3 | incompatibility: Incompatibilité 4 | pause: Mettre en pause 5 | reset: Réinitialiser 6 | resume: Continuer 7 | warning: Attention 8 | footer: 9 | credits: Crédits patates à {robynn} et {hels} 10 | hels: HelS 11 | made: Fait avec ❤️ par {praz} 12 | praz: PraZ 13 | robynn: Robynn Frauhn 14 | info: 15 | developer: 16 | contact: me contacter 17 | content: >- 18 | Cette appli est mon cadeau à une communauté adorable et bienveillante qui 19 | m'a beaucoup apporté. C'est aussi un terrain de jeu qui me permet 20 | d'expérimenter des technologies que j'apprécie. J'ai essayé de la rendre 21 | la plus accessible possible. Cependant il se peut qu'il y ait des bugs et 22 | si tu en trouves un tu peux {contact}. Si tu es un développeur, tu peux 23 | aussi {contribute}. Enfin, si tu veux contribuer d'une autre façon, tu 24 | peux toujours {kofi}. 25 | contribute: contribuer 26 | kofi: m'offrir un café 27 | title: Un mot du développeur 28 | origin: 29 | content: >- 30 | Beacoup de petites patates de la merveilleuse communauté de Robynn Frauhn 31 | utilisaient la technique Pomodoro dans leur travail. Comme dans l'oasis de 32 | Robynn, les patates reignaient, elles décidèrent de remplacer les tomates 33 | dans leur technique. C'est ainsi que naquit la technique Potato et son 34 | Potato Timer. 35 | title: L'origine du Potato Timer 36 | technique: 37 | content: >- 38 | La technique Pomodoro est une technique de gestion du temps. Elle permet 39 | de définir des périodes de travail séparées par des courtes périodes de 40 | pause. Il convient avant de commencer de définir des tâches à effectuer 41 | pendant chaque période de travail. On peut la décomposer en 5 étapes : 42 | steps: 43 | - Définir la tâche sur laquelle travailler. 44 | - Lancer un timer travail (25 minutes). 45 | - Travailler sur cette tâche pendant ces 25 minutes). 46 | - Faire une pause de 5 minutes. 47 | - Recommencer en prenant une pause de 15-30 minutes au 4e cycle. 48 | title: La technique Pomodoro 49 | title: C'est quoi Potato Timer ? 50 | not-found: Page non trouvée 51 | notification: 52 | api-unavailable: >- 53 | Ton appareil ou navigateur est incompatible avec les notifications. Tu n'en 54 | recevras pas quand il sera l'heure de prendre une pause ou de se remettre au 55 | travail. 56 | permission-warning: >- 57 | Tu n'as pas autorisé l'application à t'envoyer des notifications. Tu risques 58 | de ne pas t'apercevoir que c'est le moment de faire une pause ! 59 | request-permission: Autoriser les notifications 60 | options: 61 | change-language: Changer de langue — actuellement {current} 62 | change-theme: Changer le theme — actuellement {current} 63 | info: Informations sur l'app 64 | title: Options 65 | reset: 66 | cancel: Annuler 67 | confirm: Réinitialiser 68 | message: Il y a une patate en cours, es-tu sûr·e de vouloir réinitialiser ? 69 | settings: 70 | cancel: Annuler 71 | long-pause: Durée de la pause finale (minutes) 72 | pause-time: Durée d'une patate pause (minutes) 73 | reset: Rétablir les durées par défaut 74 | save: Sauvegarder 75 | title: Réglages 76 | work-time: Durée d'une patate travail (minutes) 77 | steps: 78 | done: Terminé ! 79 | pause: On se détend... 80 | ready: Prêt·e ? 81 | work: Au travail ! 82 | tasks: 83 | add: Ajouter une tâche 84 | delete: Supprimer la tâche "{task}" 85 | empty: Aucune tâche à effectuer. 86 | timer: Progression de la patate 87 | theme: 88 | dark: Sombre 89 | light: Clair 90 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "18" 3 | 4 | [build] 5 | publish = "dist" 6 | command = "pnpm run build" 7 | 8 | [[redirects]] 9 | from = "/*" 10 | to = "/index.html" 11 | status = 200 12 | 13 | [[headers]] 14 | for = "/manifest.webmanifest" 15 | [headers.values] 16 | Content-Type = "application/manifest+json" 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "potato-timer", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vite --port 3333", 6 | "build": "cross-env NODE_ENV=production vite build", 7 | "lint": "eslint . --fix", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@vueuse/components": "^9.13.0", 12 | "@vueuse/core": "^9.13.0", 13 | "@vueuse/head": "^1.1.9", 14 | "date-fns": "^2.29.3", 15 | "focus-trap": "^7.3.1", 16 | "focus-trap-vue": "^3.4.0", 17 | "use-persistent-stopwatch": "^1.3.0", 18 | "vue": "^3.2.47", 19 | "vue-i18n": "^9.2.2", 20 | "vue-router": "^4.1.6" 21 | }, 22 | "devDependencies": { 23 | "@antfu/eslint-config": "^0.31.1", 24 | "@iconify/json": "^2.2.28", 25 | "@intlify/vite-plugin-vue-i18n": "^6.0.3", 26 | "@vitejs/plugin-vue": "^3.2.0", 27 | "cross-env": "^7.0.3", 28 | "eslint": "^8.35.0", 29 | "naive-ui": "^2.34.3", 30 | "sass": "^1.58.3", 31 | "typescript": "^4.9.5", 32 | "unplugin-auto-import": "^0.12.2", 33 | "unplugin-icons": "^0.14.15", 34 | "unplugin-vue-components": "^0.22.12", 35 | "vite": "^3.2.5", 36 | "vite-plugin-fonts": "^0.6.0", 37 | "vite-plugin-pages": "^0.27.1", 38 | "vite-plugin-pwa": "^0.13.3", 39 | "vite-plugin-vue-layouts": "^0.7.0", 40 | "vitest": "^0.25.8", 41 | "vue-tsc": "^1.2.0" 42 | }, 43 | "volta": { 44 | "node": "16.13.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/potato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/public/potato.png -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/public/pwa-512x512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 46 | 47 | 72 | -------------------------------------------------------------------------------- /src/assets/potatoCry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoCry.png -------------------------------------------------------------------------------- /src/assets/potatoDetect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoDetect.png -------------------------------------------------------------------------------- /src/assets/potatoHappy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoHappy.png -------------------------------------------------------------------------------- /src/assets/potatoLost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoLost.png -------------------------------------------------------------------------------- /src/assets/potatoLurk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoLurk.png -------------------------------------------------------------------------------- /src/assets/potatoNap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoNap.png -------------------------------------------------------------------------------- /src/assets/potatoNote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoNote.png -------------------------------------------------------------------------------- /src/assets/potatoNotif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoNotif.png -------------------------------------------------------------------------------- /src/assets/potatoParty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoParty.png -------------------------------------------------------------------------------- /src/assets/potatoShy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazdevs/potato-timer/d95f8f57d18f2df685faa9488895125ad02aa2a7/src/assets/potatoShy.png -------------------------------------------------------------------------------- /src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | const EffectScope: typeof import('vue')['EffectScope'] 5 | const computed: typeof import('vue')['computed'] 6 | const createApp: typeof import('vue')['createApp'] 7 | const customRef: typeof import('vue')['customRef'] 8 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 9 | const defineComponent: typeof import('vue')['defineComponent'] 10 | const effectScope: typeof import('vue')['effectScope'] 11 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 12 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 13 | const h: typeof import('vue')['h'] 14 | const inject: typeof import('vue')['inject'] 15 | const isProxy: typeof import('vue')['isProxy'] 16 | const isReactive: typeof import('vue')['isReactive'] 17 | const isReadonly: typeof import('vue')['isReadonly'] 18 | const isRef: typeof import('vue')['isRef'] 19 | const markRaw: typeof import('vue')['markRaw'] 20 | const nextTick: typeof import('vue')['nextTick'] 21 | const onActivated: typeof import('vue')['onActivated'] 22 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 23 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 24 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 25 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 26 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 27 | const onDeactivated: typeof import('vue')['onDeactivated'] 28 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 29 | const onMounted: typeof import('vue')['onMounted'] 30 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 31 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 32 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 33 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 34 | const onUnmounted: typeof import('vue')['onUnmounted'] 35 | const onUpdated: typeof import('vue')['onUpdated'] 36 | const provide: typeof import('vue')['provide'] 37 | const reactive: typeof import('vue')['reactive'] 38 | const readonly: typeof import('vue')['readonly'] 39 | const ref: typeof import('vue')['ref'] 40 | const resolveComponent: typeof import('vue')['resolveComponent'] 41 | const resolveDirective: typeof import('vue')['resolveDirective'] 42 | const shallowReactive: typeof import('vue')['shallowReactive'] 43 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 44 | const shallowRef: typeof import('vue')['shallowRef'] 45 | const toRaw: typeof import('vue')['toRaw'] 46 | const toRef: typeof import('vue')['toRef'] 47 | const toRefs: typeof import('vue')['toRefs'] 48 | const triggerRef: typeof import('vue')['triggerRef'] 49 | const unref: typeof import('vue')['unref'] 50 | const useAttrs: typeof import('vue')['useAttrs'] 51 | const useCssModule: typeof import('vue')['useCssModule'] 52 | const useCssVars: typeof import('vue')['useCssVars'] 53 | const useI18n: typeof import('vue-i18n')['useI18n'] 54 | const useLink: typeof import('vue-router')['useLink'] 55 | const useRoute: typeof import('vue-router')['useRoute'] 56 | const useRouter: typeof import('vue-router')['useRouter'] 57 | const useSlots: typeof import('vue')['useSlots'] 58 | const watch: typeof import('vue')['watch'] 59 | const watchEffect: typeof import('vue')['watchEffect'] 60 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 61 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 62 | } 63 | -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | CarbonAdd: typeof import('~icons/carbon/add')['default'] 11 | CarbonClose: typeof import('~icons/carbon/close')['default'] 12 | CarbonDelete: typeof import('~icons/carbon/delete')['default'] 13 | CarbonHelp: typeof import('~icons/carbon/help')['default'] 14 | CarbonMoon: typeof import('~icons/carbon/moon')['default'] 15 | CarbonPause: typeof import('~icons/carbon/pause')['default'] 16 | CarbonPlay: typeof import('~icons/carbon/play')['default'] 17 | CarbonReset: typeof import('~icons/carbon/reset')['default'] 18 | CarbonSettings: typeof import('~icons/carbon/settings')['default'] 19 | CarbonSun: typeof import('~icons/carbon/sun')['default'] 20 | CarbonTranslate: typeof import('~icons/carbon/translate')['default'] 21 | InfoPanel: typeof import('./components/InfoPanel.vue')['default'] 22 | LanguageSelector: typeof import('./components/LanguageSelector.vue')['default'] 23 | NAlert: typeof import('naive-ui')['NAlert'] 24 | NButton: typeof import('naive-ui')['NButton'] 25 | NCard: typeof import('naive-ui')['NCard'] 26 | NCheckbox: typeof import('naive-ui')['NCheckbox'] 27 | NDivider: typeof import('naive-ui')['NDivider'] 28 | NElement: typeof import('naive-ui')['NElement'] 29 | NEllipsis: typeof import('naive-ui')['NEllipsis'] 30 | NIcon: typeof import('naive-ui')['NIcon'] 31 | NInput: typeof import('naive-ui')['NInput'] 32 | NInputGroup: typeof import('naive-ui')['NInputGroup'] 33 | NInputNumber: typeof import('naive-ui')['NInputNumber'] 34 | NLayout: typeof import('naive-ui')['NLayout'] 35 | NList: typeof import('naive-ui')['NList'] 36 | NListItem: typeof import('naive-ui')['NListItem'] 37 | NModal: typeof import('naive-ui')['NModal'] 38 | NotificationsWarning: typeof import('./components/NotificationsWarning.vue')['default'] 39 | NPopover: typeof import('naive-ui')['NPopover'] 40 | NProgress: typeof import('naive-ui')['NProgress'] 41 | NSpace: typeof import('naive-ui')['NSpace'] 42 | OptionControls: typeof import('./components/OptionControls.vue')['default'] 43 | PotatoProgress: typeof import('./components/PotatoProgress.vue')['default'] 44 | PotatoSettings: typeof import('./components/PotatoSettings.vue')['default'] 45 | PotatoTimer: typeof import('./components/PotatoTimer.vue')['default'] 46 | ResetButton: typeof import('./components/ResetButton.vue')['default'] 47 | RouterLink: typeof import('vue-router')['RouterLink'] 48 | RouterView: typeof import('vue-router')['RouterView'] 49 | TaskList: typeof import('./components/TaskList.vue')['default'] 50 | ThemeSelector: typeof import('./components/ThemeSelector.vue')['default'] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/InfoPanel.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 131 | 132 | 168 | -------------------------------------------------------------------------------- /src/components/LanguageSelector.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /src/components/NotificationsWarning.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 60 | 61 | 77 | -------------------------------------------------------------------------------- /src/components/OptionControls.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /src/components/PotatoProgress.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 59 | 60 | 71 | -------------------------------------------------------------------------------- /src/components/PotatoSettings.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 150 | 151 | 190 | -------------------------------------------------------------------------------- /src/components/PotatoTimer.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 53 | 54 | 61 | -------------------------------------------------------------------------------- /src/components/ResetButton.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 80 | 81 | 102 | -------------------------------------------------------------------------------- /src/components/TaskList.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 90 | 91 | 122 | -------------------------------------------------------------------------------- /src/components/ThemeSelector.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/composables/useNotifier.ts: -------------------------------------------------------------------------------- 1 | import { usePermission } from '@vueuse/core' 2 | import { type ComputedRef } from 'vue' 3 | 4 | import potatoNap from '~/assets/potatoNap.png' 5 | import potatoNote from '~/assets/potatoNote.png' 6 | import potatoParty from '~/assets/potatoParty.png' 7 | import { Step } from '~/composables/usePotato' 8 | 9 | export default function useNotifier(step: ComputedRef): void { 10 | const notificationAccess = usePermission('notifications') 11 | const { t } = useI18n() 12 | 13 | watch(step, (next, prev) => { 14 | if ( 15 | !window.Notification 16 | || notificationAccess.value !== 'granted' 17 | || prev === Step.ready 18 | ) 19 | return 20 | switch (next) { 21 | case Step.work: 22 | return new Notification(t('steps.work'), { icon: potatoNote }) 23 | case Step.pause: 24 | return new Notification(t('steps.pause'), { icon: potatoNap }) 25 | case Step.done: 26 | return new Notification(t('steps.done'), { icon: potatoParty }) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/composables/usePotato.ts: -------------------------------------------------------------------------------- 1 | import usePersistentStopwatch from 'use-persistent-stopwatch' 2 | import { type ComputedRef, type Ref } from 'vue' 3 | 4 | import usePotatoStorage from '~/composables/usePotatoStorage' 5 | 6 | export interface TimeSettings { 7 | workTime: number 8 | pauseTime: number 9 | longPause: number 10 | } 11 | 12 | export enum Step { 13 | ready = 'ready', 14 | done = 'done', 15 | work = 'work', 16 | pause = 'pause', 17 | } 18 | 19 | export interface Potato { 20 | running: ComputedRef 21 | currentStep: ComputedRef 22 | currentDuration: ComputedRef 23 | currentRemaining: ComputedRef 24 | currentPercentage: ComputedRef 25 | workTime: Ref 26 | reset: () => void 27 | toggle: () => void 28 | changeTimes: (ts: TimeSettings) => void 29 | } 30 | 31 | function usePotato(): Potato { 32 | const { elapsed, pause, resume, reset, running } = usePersistentStopwatch( 33 | 'potato', 34 | { interval: 1 }, 35 | ) 36 | 37 | const { workTime, pauseTime, longPause } = usePotatoStorage() 38 | 39 | const elapsedSeconds = computed(() => Math.floor(elapsed.value / 1000)) 40 | 41 | const timestamps = computed(() => 42 | [0, 1, 2, 3] 43 | .map(val => val * (workTime.value + pauseTime.value) + workTime.value) 44 | .reduce( 45 | (acc: number[], cur: number, idx: number) => [ 46 | ...acc, 47 | cur, 48 | idx === 3 ? cur + longPause.value : cur + pauseTime.value, 49 | ], 50 | [], 51 | ), 52 | ) 53 | 54 | const durations = computed(() => 55 | timestamps.value.map((val: number, idx: number, arr: number[]) => 56 | idx ? val - arr[idx - 1] : val, 57 | ), 58 | ) 59 | 60 | const currentIndex = computed(() => 61 | timestamps.value.findIndex((val: number) => { 62 | return elapsedSeconds.value < val 63 | }), 64 | ) 65 | 66 | const currentStep = computed(() => 67 | elapsed.value === 0 68 | ? Step.ready 69 | : currentIndex.value === -1 70 | ? Step.done 71 | : currentIndex.value % 2 === 0 72 | ? Step.work 73 | : Step.pause, 74 | ) 75 | 76 | const potatoRunning = computed( 77 | () => running.value && [Step.work, Step.pause].includes(currentStep.value), 78 | ) 79 | 80 | const currentDuration = computed(() => 81 | currentIndex.value < 0 82 | ? durations.value[durations.value.length - 1] 83 | : durations.value[currentIndex.value], 84 | ) 85 | 86 | const currentRemaining = computed(() => 87 | currentIndex.value < 0 88 | ? 0 89 | : timestamps.value[currentIndex.value] - elapsedSeconds.value, 90 | ) 91 | 92 | const currentPercentage = computed( 93 | () => 100 - (currentRemaining.value / currentDuration.value) * 100, 94 | ) 95 | 96 | function toggle() { 97 | if (running.value) 98 | pause() 99 | else if (currentIndex.value >= 0) 100 | resume() 101 | } 102 | 103 | function changeTimes(settings: TimeSettings) { 104 | workTime.value = settings.workTime 105 | pauseTime.value = settings.pauseTime 106 | longPause.value = settings.longPause 107 | reset() 108 | } 109 | 110 | onMounted(() => { 111 | if (currentIndex.value < 0) 112 | pause() 113 | }) 114 | 115 | watch(currentIndex, (v) => { 116 | if (v < 0) 117 | pause() 118 | }) 119 | 120 | return { 121 | running: potatoRunning, 122 | reset, 123 | currentStep, 124 | currentDuration, 125 | currentRemaining, 126 | currentPercentage, 127 | toggle, 128 | workTime, 129 | changeTimes, 130 | } 131 | } 132 | 133 | export default usePotato 134 | -------------------------------------------------------------------------------- /src/composables/usePotatoStorage.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | import { type Ref } from 'vue' 3 | 4 | export const STORAGE_WORK_TIME = 'potato-work-time' 5 | export const STORAGE_PAUSE_TIME = 'potato-pause-time' 6 | export const STORAGE_LONG_PAUSE = 'potato-long-pause' 7 | export const DEFAULT_WORK_TIME = 25 * 60 8 | export const DEFAULT_PAUSE_TIME = 5 * 60 9 | export const DEFAULT_LONG_PAUSE = 15 * 60 10 | 11 | export interface PotatoStorage { 12 | workTime: Ref 13 | pauseTime: Ref 14 | longPause: Ref 15 | } 16 | 17 | function usePotatoStorage(): PotatoStorage { 18 | const workTime = useLocalStorage(STORAGE_WORK_TIME, DEFAULT_WORK_TIME) 19 | const pauseTime = useLocalStorage(STORAGE_PAUSE_TIME, DEFAULT_PAUSE_TIME) 20 | const longPause = useLocalStorage(STORAGE_LONG_PAUSE, DEFAULT_LONG_PAUSE) 21 | 22 | return { 23 | workTime, 24 | pauseTime, 25 | longPause, 26 | } 27 | } 28 | 29 | export default usePotatoStorage 30 | -------------------------------------------------------------------------------- /src/composables/useTasks.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | import { type Ref } from 'vue' 3 | 4 | export interface Task { 5 | text: string 6 | done: boolean 7 | } 8 | 9 | export default function useTasks(): Ref { 10 | const tasks: Ref = useLocalStorage('potato-tasks', []) 11 | 12 | return tasks 13 | } 14 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 53 | 54 | 89 | -------------------------------------------------------------------------------- /src/logic/dark.ts: -------------------------------------------------------------------------------- 1 | import { useDark, useToggle } from '@vueuse/core' 2 | 3 | export const isDark = useDark() 4 | export const toggleDark = useToggle(isDark) 5 | -------------------------------------------------------------------------------- /src/logic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dark' 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createHead } from '@vueuse/head' 2 | import { createApp } from 'vue' 3 | import { createI18n } from 'vue-i18n' 4 | 5 | import messages from '@intlify/vite-plugin-vue-i18n/messages' 6 | 7 | import App from './App.vue' 8 | 9 | const app = createApp(App) 10 | 11 | //* @vueuse/head 12 | const head = createHead() 13 | app.use(head) 14 | 15 | //* vue-i18n 16 | const i18n = createI18n({ 17 | legacy: false, 18 | locale: 'fr', 19 | messages, 20 | }) 21 | app.use(i18n) 22 | 23 | app.mount('#app') 24 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { ComponentOptions } from 'vue' 3 | 4 | const component: ComponentOptions 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "incremental": false, 7 | "lib": ["DOM", "ESNext"], 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "noUnusedLocals": true, 11 | "jsx": "preserve", 12 | "paths": { 13 | "~/*": ["src/*"] 14 | }, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "target": "es2016", 20 | "types": [ 21 | "vite/client", 22 | "vite-plugin-pages/client", 23 | "vite-plugin-vue-layouts/client", 24 | "@intlify/vite-plugin-vue-i18n/client" 25 | ] 26 | }, 27 | "exclude": ["dist", "node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import VueI18n from '@intlify/vite-plugin-vue-i18n' 3 | import Vue from '@vitejs/plugin-vue' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import IconsResolver from 'unplugin-icons/resolver' 6 | import Icons from 'unplugin-icons/vite' 7 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' 8 | import Components from 'unplugin-vue-components/vite' 9 | import { defineConfig } from 'vite' 10 | import Fonts from 'vite-plugin-fonts' 11 | import { VitePWA } from 'vite-plugin-pwa' 12 | 13 | export default defineConfig({ 14 | resolve: { 15 | alias: { 16 | '~/': `${path.resolve(__dirname, 'src')}/`, 17 | }, 18 | }, 19 | plugins: [ 20 | Vue(), 21 | Components({ 22 | dts: 'src/components.d.ts', 23 | resolvers: [ 24 | NaiveUiResolver(), 25 | IconsResolver({ 26 | componentPrefix: '', 27 | }), 28 | ], 29 | }), 30 | Fonts({ 31 | google: { 32 | families: ['Quicksand'], 33 | }, 34 | }), 35 | AutoImport({ 36 | dts: 'src/auto-imports.d.ts', 37 | imports: ['vue', 'vue-i18n', 'vue-router'], 38 | }), 39 | Icons({}), 40 | VitePWA({ 41 | registerType: 'autoUpdate', 42 | manifest: { 43 | name: 'Potato Timer', 44 | short_name: 'Potato Timer', 45 | theme_color: '#ffffff', 46 | icons: [ 47 | { 48 | src: '/pwa-192x192.png', 49 | sizes: '192x192', 50 | type: 'image/png', 51 | }, 52 | { 53 | src: '/pwa-512x512.png', 54 | sizes: '512x512', 55 | type: 'image/png', 56 | }, 57 | { 58 | src: '/pwa-512x512.png', 59 | sizes: '512x512', 60 | type: 'image/png', 61 | purpose: 'any maskable', 62 | }, 63 | ], 64 | }, 65 | }), 66 | VueI18n({ 67 | include: [path.resolve(__dirname, 'locales/**')], 68 | }), 69 | ], 70 | }) 71 | --------------------------------------------------------------------------------