diff --git a/README.md b/README.md index cd125b63..4db30e6f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,77 @@ _Note: Skola is still in early-ish development. Expect bugs, missing features, a A modern, open-source spaced repetition application that reimagines flashcard learning with a focus on beautiful design, local-first architecture, and thoughtful user experience. Visit [skola.cards](https://skola.cards) now. +## Running Locally + +Skola is a `pnpm` workspace-free single-package app with two entry points: + +- the main web app powered by Vite +- an optional Tauri desktop shell in [`src-tauri`](/Users/sergeypogranichniy/WebstormProjects/skola/src-tauri) + +### Prerequisites + +- Node.js 18+ recommended +- `corepack` enabled so the pinned package manager version can be used +- Rust toolchain only if you want to run the Tauri desktop app + +Enable the package manager pinned by the repo: + +```bash +corepack enable +corepack prepare pnpm@10.28.2 --activate +``` + +Install dependencies: + +```bash +corepack pnpm install +``` + +### Run The Web App + +Start the Vite dev server: + +```bash +corepack pnpm run start +``` + +The app will be available at [http://127.0.0.1:5173](http://127.0.0.1:5173) or the equivalent localhost URL printed by Vite. + +Useful checks: + +```bash +corepack pnpm run lint:type +corepack pnpm run build +``` + +These were verified successfully in this repository on May 11, 2026. + +### Run The Tauri Desktop App + +The desktop wrapper lives in [`src-tauri`](/Users/sergeypogranichniy/WebstormProjects/skola/src-tauri) and requires a working Rust toolchain. If `cargo` is not installed, `tauri dev` will fail before startup. + +Install Rust with [rustup](https://rustup.rs/), then run: + +```bash +corepack pnpm exec tauri dev +``` + +To build the desktop app: + +```bash +corepack pnpm exec tauri build +``` + +### Repo Structure + +- [`src/app`](/Users/sergeypogranichniy/WebstormProjects/skola/src/app) contains route-level views and feature UI such as learning, deck management, cards, settings, and shell layout +- [`src/components/ui`](/Users/sergeypogranichniy/WebstormProjects/skola/src/components/ui) contains the shared component primitives and styles +- [`src/logic`](/Users/sergeypogranichniy/WebstormProjects/skola/src/logic) contains application state and domain logic for decks, cards, notes, statistics, and settings +- [`src/logic/db.ts`](/Users/sergeypogranichniy/WebstormProjects/skola/src/logic/db.ts) defines the local Dexie database and optional Dexie Cloud sync +- [`src/i18n`](/Users/sergeypogranichniy/WebstormProjects/skola/src/i18n) contains internationalization setup and locale files +- [`src/style`](/Users/sergeypogranichniy/WebstormProjects/skola/src/style) contains the design tokens and global styles +- [`src-tauri`](/Users/sergeypogranichniy/WebstormProjects/skola/src-tauri) contains the desktop packaging and native app configuration + ## Core Features **Local-First Architecture** — Your data lives in your browser using IndexedDB, ensuring privacy, speed, and complete ownership of your learning materials. No account required. @@ -30,7 +101,7 @@ AI is intentionally used sparingly – there's no AI flashcard generation. The a ## Current Limitations - Statistics view temporarily disabled during UI refactor (will return with lightweight charting) -- Notebook view experimental and under evaluation +- Cards view experimental and under evaluation - Image occlusion not yet implemented - Audio support not yet implemented - Limited note explorer and management features (e.g., bulk editing, tagging) diff --git a/package.json b/package.json index 55033727..6f7d518f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "start": "vite", "build": "vite build", "serve": "vite preview", + "test": "vitest run", "lint": "biome check", "lint:type": "tsc --noEmit", "extract": "i18next --config i18next-parser.config.mjs 'src/**/*.{ts,tsx}'", @@ -93,7 +94,8 @@ "vite": "5.2.13", "vite-plugin-checker": "0.6.4", "vite-plugin-pwa": "^1.2.0", - "vite-tsconfig-paths": "4.3.2" + "vite-tsconfig-paths": "4.3.2", + "vitest": "^2.1.9" }, "packageManager": "pnpm@10.28.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62e632ea..7bb14be9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,6 +180,9 @@ importers: vite-tsconfig-paths: specifier: 4.3.2 version: 4.3.2(typescript@5.5.2)(vite@5.2.13(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0)) + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0) packages: @@ -1891,6 +1894,35 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1966,6 +1998,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -2093,6 +2129,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2116,10 +2156,18 @@ packages: caniuse-lite@1.0.30001772: resolution: {integrity: sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -2270,6 +2318,10 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2401,6 +2453,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2492,6 +2547,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2519,6 +2577,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} engines: {node: '>= 16'} @@ -3192,6 +3254,9 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3205,6 +3270,9 @@ packages: magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -3443,6 +3511,13 @@ packages: resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} engines: {node: '>=18'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -3989,6 +4064,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -4038,10 +4116,16 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4165,10 +4249,28 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} @@ -4374,6 +4476,11 @@ packages: resolution: {integrity: sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==} engines: {node: '>=10.13.0'} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-plugin-checker@0.6.4: resolution: {integrity: sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==} engines: {node: '>=14.16'} @@ -4453,6 +4560,31 @@ packages: terser: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -4524,6 +4656,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -6288,6 +6425,46 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.2.13(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.2.13(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -6364,6 +6541,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + async-function@1.0.0: {} async@3.2.6: {} @@ -6503,6 +6682,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6526,11 +6707,21 @@ snapshots: caniuse-lite@1.0.30001772: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.3: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -6677,6 +6868,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -6846,6 +7039,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -7005,6 +7200,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -7045,6 +7244,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expect-type@1.3.0: {} + express-rate-limit@8.2.1(express@5.2.1): dependencies: express: 5.2.1 @@ -7802,6 +8003,8 @@ snapshots: lodash@4.17.23: {} + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@11.2.6: {} @@ -7814,6 +8017,10 @@ snapshots: dependencies: sourcemap-codec: 1.4.8 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -8025,6 +8232,10 @@ snapshots: path-type@6.0.0: {} + pathe@1.1.2: {} + + pathval@2.0.1: {} + peberminta@0.9.0: {} picocolors@1.1.1: {} @@ -8653,6 +8864,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -8690,8 +8903,12 @@ snapshots: sprintf-js@1.1.3: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -8858,11 +9075,21 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + tippy.js@6.3.7: dependencies: '@popperjs/core': 2.11.8 @@ -9086,6 +9313,23 @@ snapshots: - bare-abort-controller - react-native-b4a + vite-node@2.1.9(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.2.13(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vite-plugin-checker@0.6.4(eslint@9.39.3)(optionator@0.9.4)(typescript@5.5.2)(vite@5.2.13(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0)): dependencies: '@babel/code-frame': 7.29.0 @@ -9142,6 +9386,40 @@ snapshots: sugarss: 4.0.1(postcss@8.4.47) terser: 5.46.0 + vitest@2.1.9(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.2.13(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.2.13(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0) + vite-node: 2.1.9(@types/node@22.5.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.46.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.5.5 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - stylus + - sugarss + - supports-color + - terser + void-elements@3.1.0: {} vscode-jsonrpc@6.0.0: {} @@ -9235,6 +9513,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} workbox-background-sync@7.4.0: diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b0521095..c3e10a45 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,8 +6,8 @@ "build": { "frontendDist": "../dist", "devUrl": "http://localhost:5173", - "beforeDevCommand": "npm run start", - "beforeBuildCommand": "npm run build" + "beforeDevCommand": "corepack pnpm run start", + "beforeBuildCommand": "corepack pnpm run build" }, "app": { "windows": [ diff --git a/src/app/NoteTable/NoteTable.css b/src/app/NoteTable/NoteTable.css index 52af64e5..01e4c198 100644 --- a/src/app/NoteTable/NoteTable.css +++ b/src/app/NoteTable/NoteTable.css @@ -93,16 +93,39 @@ .note-table__cell { padding: var(--spacing-sm) var(--spacing-md); font-weight: 500; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; } +.note-table__cell:not(:first-child) { + white-space: nowrap; +} + .note-table__cell:first-child { width: 50%; } +.note-table__preview { + display: flex; + flex-direction: column; + gap: var(--spacing-xxs); + min-width: 0; +} + +.note-table__preview-front, +.note-table__preview-back { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.note-table__preview-back { + color: var(--theme-neutral-500); + font-size: var(--font-size-sm); + font-weight: 400; +} + .note-table__empty { padding: var(--spacing-xl) var(--spacing-md); text-align: center; diff --git a/src/app/NoteTable/NoteTableItem.tsx b/src/app/NoteTable/NoteTableItem.tsx index 0384379e..ab1f78e4 100644 --- a/src/app/NoteTable/NoteTableItem.tsx +++ b/src/app/NoteTable/NoteTableItem.tsx @@ -1,4 +1,5 @@ import { Note, NoteType } from "@/logic/note/note"; +import { getNotePreview } from "@/logic/note/preview"; import { NoteWithComparableDeckName } from "@/logic/note/sort"; import { memo } from "react"; @@ -56,7 +57,7 @@ export const NoteTableItem = memo( const getCellContent = (columnKey: string) => { switch (columnKey) { case "name": - return note.sortField; + return ; case "creationDate": return note.creationDate.toLocaleDateString(); case "noteType": @@ -107,3 +108,20 @@ export const NoteTableItem = memo( ); } ); + +function NotePreviewCell({ + note, +}: { + note: Note | NoteWithComparableDeckName; +}) { + const preview = getNotePreview(note); + + return ( +
+
{preview.front}
+ {preview.back && ( +
{preview.back}
+ )} +
+ ); +} diff --git a/src/app/notebook/NotebookCard.tsx b/src/app/cards/CardItem.tsx similarity index 66% rename from src/app/notebook/NotebookCard.tsx rename to src/app/cards/CardItem.tsx index 9d6fe77a..f0ff1a71 100644 --- a/src/app/notebook/NotebookCard.tsx +++ b/src/app/cards/CardItem.tsx @@ -4,24 +4,20 @@ import { useDisclosure } from "@/lib/hooks/useDisclosure"; import { getAdapter } from "@/logic/NoteTypeAdapter"; import { NoteType } from "@/logic/note/note"; import { Note } from "@/logic/note/note"; +import { getNotePreview } from "@/logic/note/preview"; import { Draggable } from "@hello-pangea/dnd"; import { IconDots } from "@tabler/icons-react"; import { memo, useState } from "react"; import NoteMenu from "../editor/NoteMenu"; -interface NotebookCardProps { +interface CardItemProps { index: number; note: Note; useCustomSort: boolean; showAnswer: boolean; } -function NotebookCard({ - note, - index, - useCustomSort, - showAnswer, -}: NotebookCardProps) { +function CardItem({ note, index, useCustomSort, showAnswer }: CardItemProps) { return useCustomSort ? ( {(provided, snapshot) => ( @@ -29,8 +25,8 @@ function NotebookCard({ {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef} - className={`notebook__card-wrapper ${ - snapshot.isDragging ? "notebook__card-wrapper--dragging" : "" + className={`cards-view__card-wrapper ${ + snapshot.isDragging ? "cards-view__card-wrapper--dragging" : "" }`} > @@ -38,12 +34,12 @@ function NotebookCard({ )} ) : ( -
+
); } -export default memo(NotebookCard); +export default memo(CardItem); const InnerCard = memo( ({ note, showAnswer }: { note: Note; showAnswer: boolean }) => { @@ -60,10 +56,11 @@ const InnerCard = memo( cursor: "pointer", }} > - {getAdapter(note).displayNote( - note, - showAnswer ? "strict" : answerToggled ? "optional" : "none" - )} +
; + showAnswer: boolean; + answerToggled: boolean; +}) { + if (note.content.type === NoteType.Cloze) { + return getAdapter(note).displayNote( + note, + showAnswer ? "strict" : answerToggled ? "optional" : "none" + ); + } + + const preview = getNotePreview(note); + + return ( +
+

{preview.front}

+ {preview.back && ( +
{preview.back}
+ )} +
+ ); +} diff --git a/src/app/cards/CardsView.css b/src/app/cards/CardsView.css new file mode 100644 index 00000000..b94300e1 --- /dev/null +++ b/src/app/cards/CardsView.css @@ -0,0 +1,78 @@ +.cards-view { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + width: 100%; + max-width: 100%; +} + +.cards-view__toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-xs); + flex-wrap: nowrap; +} + +.cards-view__search { + flex: 1; + max-width: 28rem; + min-width: 12rem; +} + +.cards-view__search svg { + width: var(--icon-size-sm); + height: var(--icon-size-sm); +} + +.cards-view__limit-notice { + color: var(--theme-neutral-500); + text-align: center; + padding: var(--spacing-sm); +} + +@media (max-width: 575.98px) { + .cards-view__toolbar { + align-items: stretch; + flex-wrap: wrap; + } + + .cards-view__search { + max-width: none; + min-width: 100%; + } +} + +.cards-view__list > * { + content-visibility: auto; + contain-intrinsic-size: auto 200px; +} + +.cards-view__card-wrapper { + transition: opacity 150ms ease; + margin-bottom: var(--spacing-sm); +} + +.cards-view__card-wrapper--dragging { + opacity: 0.6; +} + +.cards-view__preview { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + padding: var(--spacing-md); +} + +.cards-view__preview-front { + font-family: var(--font-serif); + font-size: var(--font-size-xl); + font-weight: 600; + margin: 0; +} + +.cards-view__preview-back { + color: var(--theme-neutral-600); + font-size: var(--font-size-md); + line-height: var(--line-height-md); +} diff --git a/src/app/notebook/NotebookView.tsx b/src/app/cards/CardsView.tsx similarity index 76% rename from src/app/notebook/NotebookView.tsx rename to src/app/cards/CardsView.tsx index 88f20f54..e7d554c4 100644 --- a/src/app/notebook/NotebookView.tsx +++ b/src/app/cards/CardsView.tsx @@ -1,6 +1,7 @@ import { Kbd } from "@/components/ui/Kbd"; import { Menu, MenuItem } from "@/components/ui/Menu"; import { Select, SelectOption } from "@/components/ui/Select"; +import { TextInput } from "@/components/ui/TextInput"; import { Tooltip } from "@/components/ui/Tooltip"; import { useHotkeys } from "@/lib/hooks/useHotkeys"; import { useListState } from "@/lib/hooks/useListState"; @@ -9,32 +10,42 @@ import { useDeckFromUrl } from "@/logic/deck/hooks/useDeckFromUrl"; import { useNotesOf } from "@/logic/note/hooks/useNotesOf"; import { NoteType } from "@/logic/note/note"; import { Note } from "@/logic/note/note"; +import { noteMatchesSearch } from "@/logic/note/search"; import { NoteSortFunction, NoteSorts } from "@/logic/note/sort"; import { DragDropContext, Droppable } from "@hello-pangea/dnd"; import { IconCalendar, IconMenuOrder, + IconSearch, IconTextCaption, } from "@tabler/icons-react"; -import { useEffect, useState } from "react"; +import { useDeferredValue, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import NotebookCard from "./NotebookCard"; -import "./NotebookView.css"; +import CardItem from "./CardItem"; +import "./CardsView.css"; -const BASE = "notebook"; -const NOTEBOOK_LIMIT = 1000; +const BASE = "cards-view"; +const CARDS_LIMIT = 5000; -export default function NotebookView() { +export default function CardsView() { const [deck] = useDeckFromUrl(); const [excludeSubDecks, setExcludeSubDecks] = useState(false); const [showAnswer, setShowAnswer] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const deferredSearchQuery = useDeferredValue(searchQuery); - const [notes] = useNotesOf(deck, excludeSubDecks, NOTEBOOK_LIMIT); + const [notes] = useNotesOf(deck, excludeSubDecks, CARDS_LIMIT); + const filteredNotes = useMemo( + () => filterNotes(notes ?? [], deferredSearchQuery), + [notes, deferredSearchQuery] + ); const [sortOption, setSortOption] = useState(sortOptions[0]); const [sortOrder] = useState<1 | -1>(1); - const [sortedNotes, setSortedNotes] = useState[]>(notes ?? []); + const [sortedNotes, setSortedNotes] = useState[]>( + filteredNotes ?? [] + ); const [useCustomSort, setUseCustomSort] = useState(false); const [customOrderTouched, setCustomOrderTouched] = useState(false); @@ -49,25 +60,34 @@ export default function NotebookView() { useEffect(() => { setUseCustomSort(sortOption.value === "custom_order"); setSortedNotes( - (notes ?? []).slice(0).sort(sortOption.sortFunction(sortOrder)) + filteredNotes.slice(0).sort(sortOption.sortFunction(sortOrder)) ); - }, [notes, sortOption, sortOrder, setSortedNotes]); + }, [filteredNotes, sortOption, sortOrder, setSortedNotes]); + + const omittedNoteCount = Math.max((deck?.notes.length ?? 0) - CARDS_LIMIT, 0); return (
+ setSearchQuery(event.currentTarget.value)} + placeholder="Search front and back..." + leftSection={} + aria-label="Search cards" + /> -
- {deck?.notes && deck?.notes?.length > NOTEBOOK_LIMIT && ( + {omittedNoteCount > 0 && (
- Currently there is a limit of {NOTEBOOK_LIMIT} notes displayed.{" "} - {deck.notes.length - NOTEBOOK_LIMIT} notes have been omitted. + {`Currently there is a limit of ${CARDS_LIMIT} cards displayed. ${omittedNoteCount} cards have been omitted.`}
)} {useCustomSort ? ( @@ -93,7 +113,7 @@ export default function NotebookView() { ); }} > - + {(provided) => (
{state.map((card, index) => ( - {sortedNotes.map((note, index) => ( - [], query: string) { + return notes.filter((note) => noteMatchesSearch(note, query)); +} + interface SortOption { value: string; icon: React.ComponentType; @@ -186,7 +210,7 @@ function SortSelect({ ); } -function NotebookMenu({ +function CardsMenu({ excludeSubDecks, setExcludeSubDecks, showAnswer, @@ -209,7 +233,7 @@ function NotebookMenu({ setExcludeSubDecks(!excludeSubDecks); }} > - {t("notebook.options.exclude-subdecks")} + {t("cards.options.exclude-subdecks")} - {t("notebook.options.show-answer")} + {t("cards.options.show-answer")} diff --git a/src/app/deck/DeckView.tsx b/src/app/deck/DeckView.tsx index 78480203..c0831579 100644 --- a/src/app/deck/DeckView.tsx +++ b/src/app/deck/DeckView.tsx @@ -13,7 +13,7 @@ import { useSuperDecks } from "@/logic/deck/hooks/useSuperDecks"; import { IconPlus } from "@tabler/icons-react"; import { t } from "i18next"; import { useNavigate } from "react-router-dom"; -import NotebookView from "../notebook/NotebookView"; +import CardsView from "../cards/CardsView"; import { AppHeaderContent } from "../shell/Header/Header"; import DeckMenu from "./DeckMenu"; import "./DeckView.css"; @@ -98,8 +98,8 @@ function DeckView() { )} - - {t("deck.notebook.title")} + + {t("deck.cards.title")} {(deck?.notes.length as number) > 0 && ( - - + + diff --git a/src/app/editor/NewNotesView.css b/src/app/editor/NewNotesView.css index 92aa3598..be799ab3 100644 --- a/src/app/editor/NewNotesView.css +++ b/src/app/editor/NewNotesView.css @@ -4,7 +4,7 @@ right: 0; left: 0; bottom: 0; - padding-top: 3.6875rem; + padding-top: calc(var(--app-header-height) + var(--safe-area-top)); padding-left: var(--safe-area-left); padding-right: var(--safe-area-right); @@ -29,6 +29,7 @@ .new-notes-view__content { flex-grow: 1; padding: var(--spacing-lg); + overflow-y: auto; } .new-notes-view__editor-header { @@ -50,9 +51,24 @@ .new-notes-view__footer { padding: var(--spacing-md); + padding-bottom: max(var(--spacing-md), var(--safe-area-bottom)); border-top: 1px solid var(--theme-neutral-200); display: flex; justify-content: space-between; align-items: center; background-color: var(--theme-neutral-50); } + +@media (max-width: 575.98px) { + .new-notes-view__content { + padding: var(--spacing-md); + } + + .new-notes-view__editor-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); + } +} diff --git a/src/app/editor/NewNotesView.tsx b/src/app/editor/NewNotesView.tsx index 0e834fdb..c0fa8ef4 100644 --- a/src/app/editor/NewNotesView.tsx +++ b/src/app/editor/NewNotesView.tsx @@ -61,7 +61,7 @@ function NewNotesView() { focusSelectNoteType, }) : null; - }, [deck, noteType, setNoteType, focusSelectNoteType]); + }, [deck, noteType, requestedFinish, setNoteType, focusSelectNoteType]); if (isReady && !deck) { return ; @@ -89,6 +89,7 @@ function NewNotesView() { label={t("note.new.adding-to-deck", { deckName: deck.name })} decks={decks} disableAll + selectedValue={deck.id} onSelect={(deckId) => navigate(`/new/${deckId}`)} /> - note.sortField.toLowerCase().includes(filter.toLowerCase()) && + noteMatchesSearch(note, filter) && (deckId === undefined || note.deck === deckId) ) .toArray() diff --git a/src/app/learn/FinishedLearningView/FinishedLearningView.css b/src/app/learn/FinishedLearningView/FinishedLearningView.css index 7c5d0f32..d2eb0e44 100644 --- a/src/app/learn/FinishedLearningView/FinishedLearningView.css +++ b/src/app/learn/FinishedLearningView/FinishedLearningView.css @@ -1,6 +1,6 @@ .finished-learning-view { - position: fixed; - inset: 0; + min-height: 100%; + width: 100%; display: flex; align-items: center; justify-content: center; @@ -10,6 +10,7 @@ padding-right: max(var(--spacing-3xl), var(--safe-area-right)); padding-bottom: max(var(--spacing-3xl), var(--safe-area-bottom)); background-color: var(--theme-neutral); + overflow-y: auto; } .finished-learning-view__content { @@ -79,13 +80,15 @@ margin-top: var(--spacing-3xl); } -/*@media (max-width: 768px) { +@media (max-width: 768px) { .finished-learning-view { padding: var(--spacing-2xl); + align-items: flex-start; } .finished-learning-view__content { gap: var(--spacing-3xl); + padding-block: var(--spacing-xl); } .finished-learning-view__title { @@ -108,6 +111,8 @@ @media (max-width: 480px) { .finished-learning-view { padding: var(--spacing-xl); + padding-top: max(var(--spacing-xl), var(--safe-area-top)); + padding-bottom: max(var(--spacing-xl), var(--safe-area-bottom)); } .finished-learning-view__title { @@ -121,6 +126,7 @@ .finished-learning-view__stats { grid-template-columns: 1fr; gap: var(--spacing-xl); + margin-top: var(--spacing-md); } .finished-learning-view__stat-value { @@ -130,4 +136,4 @@ .finished-learning-view__stat-label { font-size: 0.75rem; } -}*/ +} diff --git a/src/app/notebook/NotebookView.css b/src/app/notebook/NotebookView.css deleted file mode 100644 index 147a2270..00000000 --- a/src/app/notebook/NotebookView.css +++ /dev/null @@ -1,35 +0,0 @@ -.notebook { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - width: 100%; - max-width: 100%; -} - -.notebook__toolbar { - display: flex; - align-items: center; - justify-content: flex-end; - gap: var(--spacing-xs); - flex-wrap: nowrap; -} - -.notebook__limit-notice { - color: var(--theme-neutral-500); - text-align: center; - padding: var(--spacing-sm); -} - -.notebook__list > * { - content-visibility: auto; - contain-intrinsic-size: auto 200px; -} - -.notebook__card-wrapper { - transition: opacity 150ms ease; - margin-bottom: var(--spacing-sm); -} - -.notebook__card-wrapper--dragging { - opacity: 0.6; -} diff --git a/src/app/notebook/NotebookView.module.css b/src/app/notebook/NotebookView.module.css deleted file mode 100644 index d9f6405a..00000000 --- a/src/app/notebook/NotebookView.module.css +++ /dev/null @@ -1,43 +0,0 @@ -.card { - padding: var(--spacing-sm); - border: 1px solid var(--theme-neutral-300); - border-radius: var(--radius-md); - background: var(--theme-neutral-50); - cursor: pointer; - transition: box-shadow 150ms ease; -} - -.card:hover { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.card p, -.card div, -.card ul, -.card ol { - margin: 0; -} - -.card ul { - padding-inline-start: 1.25rem; -} - -.card img { - width: 75%; -} - -.cardWrapper { - margin-bottom: var(--spacing-xs); - background: transparent; - border-radius: var(--radius-sm); - transition: box-shadow 150ms ease; -} - -.cardWrapper:hover { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.cardWrapper.dragging { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - opacity: 0.5; -} diff --git a/src/app/settings/importexport/ImportButton.tsx b/src/app/settings/importexport/ImportButton.tsx index f7c26eb4..2be8ee5d 100644 --- a/src/app/settings/importexport/ImportButton.tsx +++ b/src/app/settings/importexport/ImportButton.tsx @@ -32,15 +32,9 @@ export default function ImportButton({ setImportStatus("error"); } }} + leftSection={isLoading ? : undefined} > - {isLoading ? ( - <> - - Importing... - - ) : ( - "Import and Add Cards" - )} + {isLoading ? "Importing..." : "Import and Add Cards"} ); } diff --git a/src/app/settings/importexport/ImportFromJSON.tsx b/src/app/settings/importexport/ImportFromJSON.tsx index 47e9f929..19585f43 100644 --- a/src/app/settings/importexport/ImportFromJSON.tsx +++ b/src/app/settings/importexport/ImportFromJSON.tsx @@ -3,12 +3,19 @@ import { Button } from "@/components/ui/Button"; import { Select } from "@/components/ui/Select"; import { Stack } from "@/components/ui/Stack"; import { Text } from "@/components/ui/Text"; -import { Deck } from "@/logic/deck/deck"; +import type { Deck } from "@/logic/deck/deck"; +import { BasicNoteTypeAdapter } from "@/logic/type-implementations/normal/BasicNote"; import { IconChevronRight } from "@tabler/icons-react"; import { useState } from "react"; import FileImport from "./FileImport"; import ImportButton from "./ImportButton"; import { ImportFromSourceProps, ImportStatus } from "./ImportModal"; +import { bulkImportBasicNotes } from "./bulkImportBasicNotes"; +import { + type ExtractedCrowdAnkiData, + importCrowdAnkiCards, + parseCrowdAnkiFile, +} from "./crowdAnkiImport"; interface ImportFromJSONProps extends ImportFromSourceProps {} @@ -22,9 +29,8 @@ export default function ImportFromJSON({ deck, }: ImportFromJSONProps) { const [step, setStep] = useState<"selectFile" | "options">("selectFile"); - const [extractedData, setExtractedData] = useState( - null - ); + const [extractedData, setExtractedData] = + useState(null); return ( {step === "selectFile" || !file ? ( @@ -44,9 +50,9 @@ export default function ImportFromJSON({