├── .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 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
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 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
35 |
36 |
37 |
38 |
44 |
45 |
46 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {{ t('info.technique.title') }}
64 |
65 |
66 |
{{ t('info.technique.content') }}
67 |
68 | - {{ t('info.technique.steps[0]') }}
69 | - {{ t('info.technique.steps[1]') }}
70 | - {{ t('info.technique.steps[2]') }}
71 | - {{ t('info.technique.steps[3]') }}
72 | - {{ t('info.technique.steps[4]') }}
73 |
74 |
75 |
76 |
77 | {{ t('info.origin.title') }}
78 |
79 |
80 | {{ t('info.origin.content') }}
81 |
82 |
83 |
84 | {{ t('info.developer.title') }}
85 |
86 |
87 |
92 |
93 |
99 | {{ t('info.developer.contact') }}
100 |
101 |
102 |
103 |
109 | {{ t('info.developer.contribute') }}
110 |
111 |
112 |
113 |
119 | {{ t('info.developer.kofi') }}
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
168 |
--------------------------------------------------------------------------------
/src/components/LanguageSelector.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/NotificationsWarning.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
25 |
26 |
30 |
31 |
32 |
33 | {{ t('notification.api-unavailable') }}
34 |
35 |
36 |
37 |
44 |
45 |
49 |
50 |
51 |
52 | {{ t('notification.permission-warning') }}
53 |
54 |
55 | {{ t('notification.request-permission') }}
56 |
57 |
58 |
59 |
60 |
61 |
77 |
--------------------------------------------------------------------------------
/src/components/OptionControls.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | {{ t('options.title') }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
34 |
--------------------------------------------------------------------------------
/src/components/PotatoProgress.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
47 |
48 |
![]()
49 |
50 | {{ remainingTime }}
51 |
52 |
53 |
54 |
55 | {{ t(`steps.${step}`) }}
56 |
57 |
58 |
59 |
60 |
71 |
--------------------------------------------------------------------------------
/src/components/PotatoSettings.vue:
--------------------------------------------------------------------------------
1 |
63 |
64 |
65 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
88 |
89 |
90 |
91 |
92 |
96 |
97 |
98 |
99 |
108 |
116 |
124 |
125 | {{ t('settings.reset') }}
126 |
127 |
128 |
129 |
130 |
131 |
137 | {{ t('settings.cancel') }}
138 |
139 |
140 | {{ t('settings.save') }}
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
190 |
--------------------------------------------------------------------------------
/src/components/PotatoTimer.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
26 |
27 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
46 |
50 |
51 |
52 |
53 |
54 |
61 |
--------------------------------------------------------------------------------
/src/components/ResetButton.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 | {{ t('reset.message') }}
61 |
62 |
63 |
69 | {{ t('reset.cancel') }}
70 |
71 |
72 | {{ t('reset.confirm') }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
102 |
--------------------------------------------------------------------------------
/src/components/TaskList.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
![]()
29 |
30 | {{ t('tasks.empty') }}
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{ task.text }}
38 |
39 |
40 |
41 | tasks.splice(idx, 1)"
49 | >
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
72 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
122 |
--------------------------------------------------------------------------------
/src/components/ThemeSelector.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | toggleDark()">
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
51 |
52 |
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 |
2 |
7 |
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 |
--------------------------------------------------------------------------------