├── data └── ignore ├── .env ├── .dockerignore ├── .npmrc ├── src ├── app.css ├── lib │ ├── index.ts │ ├── stores │ │ └── store.ts │ ├── components │ │ ├── ThemeSelector.svelte │ │ ├── CreateTag.svelte │ │ └── TagDistribution.svelte │ ├── timeframes.ts │ └── server │ │ └── db │ │ └── index.ts ├── routes │ ├── +page.server.ts │ ├── +layout.svelte │ ├── api │ │ ├── update │ │ │ └── +server.ts │ │ ├── weekly │ │ │ └── +server.ts │ │ ├── monthly │ │ │ └── +server.ts │ │ ├── yearly │ │ │ └── +server.ts │ │ ├── daily │ │ │ └── +server.ts │ │ └── tags │ │ │ └── +server.ts │ └── +page.svelte ├── app.d.ts └── app.html ├── static ├── borat.png ├── favicon.png └── on_time.mp3 ├── readmeAssests ├── custom.gif ├── analytics.png └── homeScreen.png ├── postcss.config.js ├── .gitignore ├── docker-compose.yml ├── vite.config.ts ├── tailwind.config.js ├── Dockerfile ├── tsconfig.json ├── svelte.config.js ├── package.json └── README.md /data/ignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DB_PATH = "data/tempus.db" -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .svelte-kit 3 | build 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /static/borat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorSatori/tempus/HEAD/static/borat.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorSatori/tempus/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/on_time.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorSatori/tempus/HEAD/static/on_time.mp3 -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /readmeAssests/custom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorSatori/tempus/HEAD/readmeAssests/custom.gif -------------------------------------------------------------------------------- /readmeAssests/analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorSatori/tempus/HEAD/readmeAssests/analytics.png -------------------------------------------------------------------------------- /readmeAssests/homeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorSatori/tempus/HEAD/readmeAssests/homeScreen.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/stores/store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | // Create a writable store for addTag 4 | export const addTagStore = writable(false); 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env.* 7 | !.env.example 8 | vite.config.js.timestamp-* 9 | vite.config.ts.timestamp-* 10 | data/tempus.db 11 | build -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | image: satoridigital/tempus:latest 5 | ports: 6 | - '9111:9111' 7 | volumes: 8 | - './data:/data' 9 | restart: unless-stopped -------------------------------------------------------------------------------- /src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { createDB } from '$lib/server/db'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | export const load = (async () => { 5 | createDB(); 6 | return {}; 7 | }) satisfies PageServerLoad; -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | server: { 7 | port: 9111, 8 | strictPort: true, 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | 9 | 10 | daisyui: { 11 | themes: ["light", "dark", "synthwave", "luxury", "forest", "dracula"], 12 | }, 13 | 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/api/update/+server.ts: -------------------------------------------------------------------------------- 1 | import { addRecord } from '$lib/server/db'; 2 | import {json} from '@sveltejs/kit'; 3 | 4 | import type { RequestEvent, RequestHandler } from './$types'; 5 | 6 | export const POST: RequestHandler = async ({request}) => { 7 | 8 | const {date, selectedTag, totalTime} = await request.json(); 9 | addRecord(date, selectedTag, totalTime); 10 | 11 | return new Response(); 12 | }; -------------------------------------------------------------------------------- /src/routes/api/weekly/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import { getWeekly } from '$lib/server/db'; 3 | import type { RequestHandler } from './$types'; 4 | 5 | export const GET: RequestHandler = async (request) => { 6 | 7 | 8 | const date = request.url.searchParams.get('date'); 9 | let entries; 10 | 11 | if (date !== null){ 12 | entries = getWeekly(date); 13 | } 14 | 15 | return json(entries); 16 | }; -------------------------------------------------------------------------------- /src/routes/api/monthly/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import { getMonthly } from '$lib/server/db'; 3 | import type { RequestHandler } from './$types'; 4 | 5 | export const GET: RequestHandler = async (request) => { 6 | 7 | const date = request.url.searchParams.get('date'); 8 | let entries; 9 | 10 | if (date !== null){ 11 | entries = getMonthly(date); 12 | } 13 | 14 | 15 | return json(entries); 16 | }; -------------------------------------------------------------------------------- /src/routes/api/yearly/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import { getYearly } from '$lib/server/db'; 3 | import type { RequestHandler } from './$types'; 4 | 5 | export const GET: RequestHandler = async (request) => { 6 | 7 | 8 | const date = request.url.searchParams.get('date'); 9 | let entries; 10 | 11 | if (date !== null){ 12 | entries = getYearly(date); 13 | } 14 | 15 | return json(entries); 16 | }; -------------------------------------------------------------------------------- /src/routes/api/daily/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { getDaily } from '$lib/server/db'; 3 | import { json } from '@sveltejs/kit'; 4 | 5 | 6 | 7 | export const GET: RequestHandler = async (request) => { 8 | 9 | const date = request.url.searchParams.get('date'); 10 | let entries; 11 | 12 | if (date !== null){ 13 | entries = getDaily(date); 14 | } 15 | 16 | 17 | return json(entries); 18 | }; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-slim as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN echo "DB_PATH=/data/tempus.db" > .env 8 | 9 | RUN npm install 10 | 11 | RUN npm run build 12 | 13 | FROM node:lts-slim 14 | 15 | WORKDIR /app 16 | 17 | COPY --from=builder /app/build /app/build 18 | COPY --from=builder /app/package.json /app/package-lock.json /app/ 19 | 20 | RUN npm install 21 | 22 | ENV PORT=9111 23 | 24 | EXPOSE 9111 25 | 26 | CMD ["node", "build/index.js"] -------------------------------------------------------------------------------- /src/routes/api/tags/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import { addTag, getAllTags } from '$lib/server/db'; 3 | import type { RequestHandler } from './$types'; 4 | 5 | 6 | export const GET: RequestHandler = async () => { 7 | 8 | const allTags = getAllTags(); 9 | return json(allTags); 10 | }; 11 | 12 | 13 | export const POST: RequestHandler = async ({request}) => { 14 | 15 | const newTag = await request.json(); 16 | addTag(newTag.newTag); 17 | 18 | return new Response(); 19 | }; -------------------------------------------------------------------------------- /src/lib/components/ThemeSelector.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter(), 15 | 16 | } 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tempus", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "start": "npm run dev" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/adapter-auto": "^2.0.0", 15 | "@sveltejs/adapter-node": "^1.3.1", 16 | "@sveltejs/kit": "^1.20.4", 17 | "@types/better-sqlite3": "^7.6.4", 18 | "@types/d3": "^7.4.0", 19 | "autoprefixer": "^10.4.14", 20 | "daisyui": "^3.5.0", 21 | "postcss": "^8.4.27", 22 | "svelte": "^4.0.5", 23 | "svelte-check": "^3.4.3", 24 | "tailwindcss": "^3.3.3", 25 | "tslib": "^2.4.1", 26 | "typescript": "^5.0.0", 27 | "vite": "^4.4.2" 28 | }, 29 | "type": "module", 30 | "dependencies": { 31 | "better-sqlite3": "^8.5.0", 32 | "d3": "^7.8.5", 33 | "theme-change": "^2.5.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/components/CreateTag.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 |
31 | 38 | {#if isValidTagInput && newTag.length > 0} 39 | 40 | {:else} 41 |

Only letters and spaces can be used for a tag.

42 | {/if} 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/lib/timeframes.ts: -------------------------------------------------------------------------------- 1 | export function getRollingSevenDayPeriod(currentDay: string){ 2 | const today = new Date(currentDay); 3 | const sevenDaysAgo = new Date(today); 4 | sevenDaysAgo.setDate(today.getDate() - 6); 5 | 6 | return { 7 | sevenDaysAgo: sevenDaysAgo.toISOString().split('T')[0], 8 | today: today.toISOString().split('T')[0], 9 | }; 10 | } 11 | 12 | export function getFirstAndLastDateOfCurrentMonth(month: string): { firstDate: string; lastDate: string } { 13 | 14 | // get first day of month 15 | const today = new Date(month); 16 | const firstDate = new Date(today.getFullYear(), today.getMonth(), 1).toISOString().split('T')[0]; 17 | 18 | // get last day of month 19 | const lastDate = new Date(today.getFullYear(), today.getMonth() + 1, 1).toISOString().split('T')[0]; 20 | return { firstDate, lastDate }; 21 | 22 | } 23 | 24 | export function getFirstAndLastDateOfYear(year: string): { firstDate: string; lastDate: string } { 25 | const firstDate = new Date(year); 26 | firstDate.setMonth(0); // January 27 | firstDate.setDate(0); // First day of the year 28 | 29 | const lastDate = new Date(year); 30 | lastDate.setFullYear(lastDate.getFullYear()); // Current year 31 | lastDate.setMonth(11); // December 32 | lastDate.setDate(31); // Last day of the year 33 | 34 | return { 35 | firstDate: firstDate.toISOString().split('T')[0], 36 | lastDate: lastDate.toISOString().split('T')[0], 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/components/TagDistribution.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 |

Tag Distribution

33 | 35 |
36 | 37 | 38 | {#each tagData as item} 39 | 40 | 41 | 42 | 49 | 50 | {/each} 51 | 52 |
{item.label}{item.value}% 43 | {#if convertData(item.time_focused)[1] === 0} 44 | {convertData(item.time_focused)[0]} M 45 | {:else} 46 | {convertData(item.time_focused)[1]} H {convertData(item.time_focused)[0]} M 47 | {/if} 48 |
53 |
54 |
55 |
56 |
57 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Boost Your Productivity with our Self-Hosted Study Tracker! 📚 2 | 3 | Are you tired of constantly juggling different productivity apps, only to find none of them truly cater to your study needs? Look no further – we're thrilled to introduce our powerful Self-Hosted Study Tracker, designed to revolutionize your study routine and help you excel like never before. 4 | 5 | ![](readmeAssests/homeScreen.png) 6 | 7 | 8 | # 🌟 Why Choose Our Self-Hosted Study Tracker? 9 | 10 | ✅ Total Control: Hosting your own instance means you're in complete control of your data. No more worries about your information being stored on external servers – it's all on your terms. 11 | 12 | ✅ Tailored to You: Our study tracker is fully customizable to fit your unique workflow. From session durations and study goals to advanced analytics, everything can be tailored to meet your preferences. 13 | 14 | ✅ Privacy Matters: Your study data is sensitive – that's why our self-hosted solution ensures your information remains private and secure at all times. 15 | 16 | ✅ Community Support: Join a vibrant community of like-minded learners and enthusiasts. Share tips, tricks, and ideas to enhance your study strategy even further. 17 | 18 | 19 | # Installation Guide 20 | 21 | Discover seamless ways to install and unleash the potential of our productivity manager on your server. Choose the method that suits you best and take control of your study routine like never before. 22 | 23 | ## Docker Installation 24 | 25 | 1. **Retrieve the Latest Docker Image** 26 | 27 | Open your terminal and enter: 28 | 29 | ```sh 30 | docker pull satoridigital/tempus:latest 31 | ``` 32 | 33 | 2. **Configure a docker-compose file** 34 | 35 | ```yaml 36 | version: '3' 37 | services: 38 | app: 39 | image: satoridigital/tempus:latest 40 | ports: 41 | - '9111:9111' 42 | volumes: 43 | - './data:/data' 44 | restart: unless-stopped 45 | ``` 46 | 47 | 3. **Run & Enjoy** 48 | 49 | ```sh 50 | docker-compose up 51 | ``` 52 | 53 | 54 | 55 | alternatively if you want to run on homeserver in daemon mode 56 | 57 | ```sh 58 | docker-compose up -d 59 | ``` 60 | 61 | ### Build from source 62 | 63 | 64 | 1. Clone the repo 65 | ```sh 66 | git clone https://github.com/TrevorSatori/tempus.git 67 | ``` 68 | 69 | 2. change directory to the repo 70 | ```sh 71 | cd tempus 72 | ``` 73 | 74 | 3. Install the dependencies 75 | ```sh 76 | npm install 77 | ``` 78 | 79 | 4.build an optimized version of the project 80 | ```sh 81 | npm run build 82 | ``` 83 | 84 | 5. run and enjoy 85 | ```sh 86 | npm run start 87 | ``` 88 | 89 | 90 | ## Analytics 91 | 92 | ![](readmeAssests/analytics.png) 93 | 94 | 95 | ## Customizable 96 | 97 | ![](readmeAssests/custom.gif) 98 | -------------------------------------------------------------------------------- /src/lib/server/db/index.ts: -------------------------------------------------------------------------------- 1 | import { DB_PATH } from '$env/static/private'; 2 | import Database from 'better-sqlite3'; 3 | import * as fs from 'fs'; 4 | import { getRollingSevenDayPeriod, getFirstAndLastDateOfCurrentMonth, getFirstAndLastDateOfYear } from '$lib/timeframes'; 5 | 6 | // if database file doesn't exist, creates it. Else connection is made. 7 | let db: any; 8 | 9 | // if Database has not been created, create one. For debug { verbose: console.log } 10 | export function createDB(){ 11 | 12 | if (!fs.existsSync(DB_PATH)){ 13 | db = new Database(DB_PATH,); 14 | const base_partition = db.prepare("Create TABLE focus (id INTEGER PRIMARY KEY, session datetime default current_timestamp, tag_id TEXT, time_focused INTEGER);"); 15 | const tagsTable = db.prepare("Create TABLE tags (id INTEGER PRIMARY KEY, name TEXT UNIQUE);"); 16 | base_partition.run(); 17 | tagsTable.run(); 18 | addTag("Work"); 19 | addTag("Study"); 20 | } else { 21 | db = new Database(DB_PATH); 22 | } 23 | } 24 | 25 | 26 | // add time studied to database 27 | export function addRecord(session: Date, tag_id: string, time_focused: number){ 28 | const stmt = db.prepare("INSERT INTO focus (session, tag_id, time_focused) VALUES (?, ?, ?)"); 29 | stmt.run(session, tag_id, time_focused); 30 | } 31 | 32 | 33 | export function getDaily(day: string){ 34 | 35 | // const day = new Date().toISOString().split('T')[0] 36 | const stmt = db.prepare("SELECT session, time_focused, tag_id FROM focus WHERE date(session) = ?"); 37 | let entries = stmt.all(day); 38 | 39 | return entries; 40 | } 41 | 42 | 43 | export function getWeekly(currentDay: string){ 44 | 45 | const {sevenDaysAgo, today} = getRollingSevenDayPeriod(currentDay); 46 | const stmt = db.prepare("SELECT session, time_focused, tag_id FROM focus WHERE date(session) >= ? AND date(session) <= ?"); 47 | let entries = stmt.all(sevenDaysAgo, today); 48 | 49 | return entries; 50 | } 51 | 52 | 53 | export function getMonthly(month: string){ 54 | 55 | 56 | const { firstDate, lastDate } = getFirstAndLastDateOfCurrentMonth(month); 57 | const stmt = db.prepare("SELECT session, time_focused, tag_id FROM focus WHERE date(session) >= ? AND date(session) < ?"); 58 | let entries = stmt.all(firstDate, lastDate); 59 | return entries; 60 | } 61 | 62 | 63 | 64 | export function getYearly(year: string){ 65 | 66 | const {firstDate, lastDate} = getFirstAndLastDateOfYear(year); 67 | const stmt = db.prepare("SELECT session, time_focused, tag_id FROM focus WHERE date(session) >= ? AND date(session) < ?"); 68 | let entries = stmt.all(firstDate, lastDate); 69 | 70 | return entries; 71 | } 72 | 73 | 74 | 75 | export function addTag(name: string){ 76 | const stmt = db.prepare("INSERT INTO tags (name) VALUES (?)"); 77 | stmt.run(name); 78 | } 79 | 80 | 81 | // returns all tags in database 82 | export function getAllTags(){ 83 | const stmt = db.prepare("SELECT name from tags"); 84 | let tags = stmt.all(); 85 | 86 | return tags; 87 | } 88 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 441 | {#if isTimer && totalTime === 0 && showAlert} 442 |
443 | 444 | Your session has been completed! 445 |
446 | {/if} 447 | 448 | {#if ($addTagStore === true)} 449 | 450 | {/if} 451 | 452 | 453 | 454 |
455 | 456 |
457 | failed to load 463 |
464 |
465 |
{selectedTag}
466 |
467 | {#if isTimer} 468 | 469 | : 470 | : 471 | 472 | 473 | {:else} 474 | 475 | : 476 | : 477 | 478 | 479 | {/if} 480 | {#if totalTime < (60 * 5) && isFocused} 481 |

(Sessions less than five minutes won't be recorded)

482 | {/if} 483 |
484 | 485 | {#if !isFocused} 486 |
487 |
Stopwatch
488 |
489 |
Timer
490 |
491 | 492 | {#if isTimer} 493 | 494 | {/if} 495 | {/if} 496 | 497 | {#if !isFocused} 498 | 499 | {:else if (isFocused && totalTime !== 0) || (isFocused && !isTimer)} 500 | 501 | {:else if isFocused && totalTime === 0 && isTimer} 502 | 503 | {/if} 504 |
505 |
506 | 507 | 508 | {#if !isFocused && wannaSee} 509 |
510 |
511 |
512 |
513 | 514 |
515 | 518 | {#if selectedAnalysis === Analysis.Daily} 519 |

{selectedDate.toISOString().split('T')[0]}

520 | {:else if selectedAnalysis === Analysis.Weekly} 521 |

{startTimeFrame} - {endTimeFrame}

522 | {:else if selectedAnalysis === Analysis.Monthly} 523 |

{selectedDate.toLocaleDateString('en-US', {month: 'short', year: 'numeric'})}

524 | {:else if selectedAnalysis === Analysis.Yearly} 525 |

{selectedDate.toLocaleDateString('en-US', {year: 'numeric'})}

526 | {/if} 527 | 530 | 531 |
532 | 533 |
534 | 535 | 536 | 537 |
538 |
539 |
540 | {#if hoursAnalytics < 1} 541 |

You've studied a total of {minutesAnalytics} minutes over {sessions} sessions

542 | {:else} 543 |

You've studied a total of {hoursAnalytics} hours {minutesAnalytics - (hoursAnalytics * 60)} minutes over {sessions} sessions

544 | {/if} 545 |
546 | {/if} 547 | 548 | 562 | --------------------------------------------------------------------------------