diff --git a/dev.db b/dev.db index 2d75c15..a0c488e 100644 Binary files a/dev.db and b/dev.db differ diff --git a/src/app/(protected)/[classSlug]/flashcards/[deckId]/page.tsx b/src/app/(protected)/[classSlug]/flashcards/[deckId]/page.tsx index 39cae8a..726de4e 100644 --- a/src/app/(protected)/[classSlug]/flashcards/[deckId]/page.tsx +++ b/src/app/(protected)/[classSlug]/flashcards/[deckId]/page.tsx @@ -90,9 +90,9 @@ export default function DeckStudyPage() { } return ( -
+
{/* Header */} -
+
-

{deck.name}

+

Flashcard session

+

{deck.name}

{deck.description && (

{deck.description}

)} @@ -126,7 +127,7 @@ export default function DeckStudyPage() {
-
+
-
{isCreatingGroup && ( -
+
-
-

Edit Deck

+
+

Edit deck

{groupedDecks.map(group => ( -
+
{editingGroupId === group.id ? (
@@ -456,7 +456,7 @@ export default function FlashcardsPage() { -

{group.name}

+

{group.name}

)} @@ -484,7 +484,7 @@ export default function FlashcardsPage() { ))} {/* Uncategorized */} -
+
-
{isCreatingGroup && ( -
+
-
-

Edit Quiz

+
+

Edit quiz

{groupedQuizzes.map(group => ( -
+
{editingGroupId === group.id ? (
@@ -431,7 +431,7 @@ export default function QuizzesPage() { -

{group.name}

+

{group.name}

)} @@ -459,7 +459,7 @@ export default function QuizzesPage() { ))} {/* Uncategorized */} -
+
- -
- - {/* Create class inline form */} - {showCreate && ( -
-
- setNewName(e.target.value)} - placeholder="e.g. PHA 109, Anatomy, Nursing Fundamentals..." - autoFocus - className="flex-1 px-3.5 py-2.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200" - /> - - -
-
- )} - - {/* Loading state */} - {loading && ( -
- {[1, 2, 3].map((i) => ( -
-
-
+
+ {[[classes.length, "Classes"], [deckTotal, "Decks"], [quizTotal, "Quizzes"]].map(([value, label]) => ( +
+
{value}
+
{label}
))}
+ + + {showCreate && ( +
+
+

Create a class

+

Give this study space a short, memorable name.

+
+
+ setNewName(e.target.value)} placeholder="e.g. Pathophysiology" autoFocus className="min-h-12 flex-1 rounded-xl border border-border bg-bg-surface-alt/60 px-4 text-text-heading placeholder:text-text-muted focus:border-primary" /> + + +
+
)} - {/* Empty state */} - {!loading && classes.length === 0 && ( -
-
- - - -
-

- No classes yet -

-

- Create your first class to start studying -

- +
+
+

Study spaces

+

Your classes

+
+

Flashcards and quizzes, organized by class.

+
+ + {loading && ( +
+ {[1, 2, 3].map((i) =>
)} +
+ )} + + {!loading && classes.length === 0 && ( +
+
+
+

Your desk is ready

+

Create your first class, then add flashcards or practice quizzes.

+
)} - {/* Class grid */} {!loading && classes.length > 0 && ( -
- {classes.map((cls) => ( -
- +
+ {classes.map((cls, index) => ( +
+
+ +
+
{cls.name.slice(0, 2).toUpperCase()}
+ +
{editingId === cls.id ? ( - { - e.preventDefault(); - e.stopPropagation(); - }} - onChange={(e) => setEditName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - handleSaveEdit(cls.id); - } else if (e.key === "Escape") { - e.preventDefault(); - e.stopPropagation(); - setEditingId(null); - } - }} - onBlur={() => handleSaveEdit(cls.id)} - autoFocus - disabled={savingEdit} - className="w-[calc(100%-2.5rem)] text-lg font-semibold text-text-heading px-2 py-1 -ml-2 rounded bg-bg-surface-alt border border-primary/30 focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" - /> - ) : ( -

- {cls.name} -

- )} -
- - - - - {cls._count.decks} {cls._count.decks === 1 ? "deck" : "decks"} - - - - - - {cls._count.quizSets} {cls._count.quizSets === 1 ? "quiz" : "quizzes"} - + { e.preventDefault(); e.stopPropagation(); }} onChange={(e) => setEditName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleSaveEdit(cls.id); } else if (e.key === "Escape") setEditingId(null); }} onBlur={() => handleSaveEdit(cls.id)} autoFocus disabled={savingEdit} className="w-full rounded-lg border border-primary bg-bg-surface-alt px-2 py-1 text-lg font-bold text-text-heading" /> + ) :

{cls.name}

} +
+ {cls._count.decks} {cls._count.decks === 1 ? "deck" : "decks"} + {cls._count.quizSets} {cls._count.quizSets === 1 ? "quiz" : "quizzes"}
- - {/* Actions */} -
- {/* Delete button */} - - {/* Edit button */} - +
+ +
-
+
))}
)} diff --git a/src/app/globals.css b/src/app/globals.css index cfd5955..53aab02 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,64 +2,104 @@ @layer base { :root { - --theme-bg-base: #f0f4f2; - --theme-bg-surface: #ffffff; - --theme-bg-surface-alt: #f4f7f5; - --theme-bg-callout: #e8f4f0; - --theme-primary: #2a7d7d; - --theme-primary-hover: #1f6363; - --theme-primary-light: #3a9e9e; - --theme-text-heading: #1a2f2f; - --theme-text-body: #2d4a4a; - --theme-text-secondary: #4a6a6a; - --theme-text-muted: #7a9a9a; - --theme-border: #c8d8d0; - --theme-border-light: #dce8e2; - --theme-success: #2d8a4e; - --theme-success-bg: #e8f5ed; - --theme-error: #c44c4c; - --theme-error-bg: #fce8e8; - --theme-badge-bg: #e0eeea; - --theme-badge-text: #2a6a5a; - --theme-danger: #dc3545; - --theme-danger-hover: #c82333; - - --theme-shadow-card: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04); - --theme-shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04); - --theme-shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.15), 0 8px 20px rgba(0, 0, 0, 0.1); + --theme-bg-base: #f5f2ea; + --theme-bg-surface: #fffdf8; + --theme-bg-surface-alt: #eeeae0; + --theme-bg-callout: #ebe9ff; + --theme-primary: #4f46e5; + --theme-primary-hover: #3f37c9; + --theme-primary-light: #756ef0; + --theme-accent: #f97360; + --theme-text-heading: #172033; + --theme-text-body: #344054; + --theme-text-secondary: #667085; + --theme-text-muted: #8b91a0; + --theme-border: #d7d2c7; + --theme-border-light: #e7e2d8; + --theme-success: #25845f; + --theme-success-bg: #e3f4ec; + --theme-error: #c44747; + --theme-error-bg: #fae8e5; + --theme-badge-bg: #e8e5ff; + --theme-badge-text: #4840bb; + --theme-danger: #c44747; + --theme-danger-hover: #a93636; + --theme-shadow-card: 0 1px 2px rgba(23, 32, 51, 0.04), 0 8px 24px rgba(23, 32, 51, 0.055); + --theme-shadow-card-hover: 0 2px 4px rgba(23, 32, 51, 0.05), 0 18px 40px rgba(23, 32, 51, 0.1); + --theme-shadow-modal: 0 28px 80px rgba(23, 32, 51, 0.22); } :root.dark { - --theme-bg-base: #121212; - --theme-bg-surface: #1e1e1e; - --theme-bg-surface-alt: #2d2d2d; - --theme-bg-callout: #1a2723; - --theme-primary: #4ab2b2; - --theme-primary-hover: #5ec7c7; - --theme-primary-light: #6cd1d1; - --theme-text-heading: #f9fafb; - --theme-text-body: #e5e7eb; - --theme-text-secondary: #9ca3af; - --theme-text-muted: #6b7280; - --theme-border: #374151; - --theme-border-light: #2d3748; - --theme-success: #4ade80; - --theme-success-bg: #064e3b; - --theme-error: #f87171; - --theme-error-bg: #7f1d1d; - --theme-badge-bg: #1f2937; - --theme-badge-text: #5ec7c7; - --theme-danger: #ef4444; - --theme-danger-hover: #f87171; + --theme-bg-base: #0d1117; + --theme-bg-surface: #151b23; + --theme-bg-surface-alt: #1c2430; + --theme-bg-callout: #25234a; + --theme-primary: #8b83ff; + --theme-primary-hover: #a49eff; + --theme-primary-light: #b2adff; + --theme-accent: #ff8a75; + --theme-text-heading: #f6f3ec; + --theme-text-body: #d8dde6; + --theme-text-secondary: #aab3c0; + --theme-text-muted: #7f8998; + --theme-border: #354050; + --theme-border-light: #293342; + --theme-success: #58c596; + --theme-success-bg: #173c30; + --theme-error: #ff8177; + --theme-error-bg: #472424; + --theme-badge-bg: #2d2959; + --theme-badge-text: #bbb7ff; + --theme-danger: #ff8177; + --theme-danger-hover: #ffa098; + --theme-shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 10px 30px rgba(0, 0, 0, 0.18); + --theme-shadow-card-hover: 0 2px 4px rgba(0, 0, 0, 0.36), 0 20px 44px rgba(0, 0, 0, 0.28); + --theme-shadow-modal: 0 30px 90px rgba(0, 0, 0, 0.55); + } - --theme-shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.2); - --theme-shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.4); - --theme-shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.5), 0 8px 20px rgba(0, 0, 0, 0.3); + * { + border-color: var(--theme-border-light); + } + + html { + background: var(--theme-bg-base); + scroll-behavior: smooth; + } + + body { + min-width: 320px; + background: + radial-gradient(circle at 88% -10%, color-mix(in srgb, var(--theme-primary) 9%, transparent), transparent 32rem), + var(--theme-bg-base); + color: var(--theme-text-body); + font-family: var(--font-manrope), ui-sans-serif, system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + } + + button, a, input, textarea, select { + outline-color: var(--theme-primary); + } + + :focus-visible { + outline: 3px solid color-mix(in srgb, var(--theme-primary) 45%, transparent); + outline-offset: 3px; + } + + button, [role="button"], a { + -webkit-tap-highlight-color: transparent; + } + + button:not(:disabled), [role="button"], a { + cursor: pointer; + } + + ::selection { + background: color-mix(in srgb, var(--theme-primary) 24%, transparent); } } @theme inline { - /* ── Sage / Teal palette mapped from CSS variables ── */ --color-bg-base: var(--theme-bg-base); --color-bg-surface: var(--theme-bg-surface); --color-bg-surface-alt: var(--theme-bg-surface-alt); @@ -67,6 +107,7 @@ --color-primary: var(--theme-primary); --color-primary-hover: var(--theme-primary-hover); --color-primary-light: var(--theme-primary-light); + --color-accent: var(--theme-accent); --color-text-heading: var(--theme-text-heading); --color-text-body: var(--theme-text-body); --color-text-secondary: var(--theme-text-secondary); @@ -81,123 +122,72 @@ --color-badge-text: var(--theme-badge-text); --color-danger: var(--theme-danger); --color-danger-hover: var(--theme-danger-hover); - - /* Typography */ - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; - - /* Shadows */ + --font-sans: var(--font-manrope), ui-sans-serif, system-ui, sans-serif; + --font-serif: var(--font-newsreader), ui-serif, Georgia, serif; --shadow-card: var(--theme-shadow-card); --shadow-card-hover: var(--theme-shadow-card-hover); --shadow-modal: var(--theme-shadow-modal); - - /* Animations */ - --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + --ease-spring: cubic-bezier(0.22, 1, 0.36, 1); } -/* ── Base Styles ── */ -body { - background: var(--color-bg-base); - color: var(--color-text-body); - font-family: var(--font-sans); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--color-border); border: 2px solid transparent; border-radius: 999px; background-clip: padding-box; } + +.app-page { + width: 100%; + max-width: 1280px; + margin-inline: auto; + padding: 2rem clamp(1rem, 3vw, 3rem) 5rem; } -/* ── Scrollbar Styling ── */ -::-webkit-scrollbar { - width: 6px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: var(--color-border); - border-radius: 3px; -} -::-webkit-scrollbar-thumb:hover { - background: var(--color-text-muted); +.editorial-title { + font-family: var(--font-newsreader), Georgia, serif; + font-weight: 600; + letter-spacing: -0.035em; } -/* ── Card flip animation ── */ -.perspective-1000 { - perspective: 1000px; +.paper-grid { + background-image: radial-gradient(color-mix(in srgb, var(--theme-text-muted) 18%, transparent) 0.7px, transparent 0.7px); + background-size: 18px 18px; } -.preserve-3d { - transform-style: preserve-3d; -} +.perspective-1000 { perspective: 1000px; } +.preserve-3d { transform-style: preserve-3d; } +.backface-hidden { backface-visibility: hidden; } +.rotate-y-180 { transform: rotateY(180deg); } +.rotate-x-180 { transform: rotateX(180deg); } -.backface-hidden { - backface-visibility: hidden; -} +@keyframes swipeLeft { to { transform: translateX(-44px) rotate(-6deg); opacity: 0; } } +@keyframes swipeRight { to { transform: translateX(44px) rotate(6deg); opacity: 0; } } +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes slideUp { from { opacity: 0; transform: translateY(14px) scale(.985); } to { opacity: 1; transform: translateY(0) scale(1); } } +@keyframes subtlePulse { 0%, 100% { opacity: 1; } 50% { opacity: .58; } } -.rotate-y-180 { - transform: rotateY(180deg); -} +.animate-swipe-left { animation: swipeLeft .36s var(--ease-spring) forwards; } +.animate-swipe-right { animation: swipeRight .36s var(--ease-spring) forwards; } +.animate-fade-in { animation: fadeIn .2s ease-out; } +.animate-slide-up { animation: slideUp .32s var(--ease-spring); } +.animate-subtle-pulse { animation: subtlePulse 1.7s ease-in-out infinite; } -.rotate-x-180 { - transform: rotateX(180deg); -} - -/* ── Swipe animations ── */ -@keyframes swipeLeft { - 0% { transform: translateX(0) rotate(0deg); opacity: 1; } - 100% { transform: translateX(-40px) rotate(-8deg); opacity: 0; } -} - -@keyframes swipeRight { - 0% { transform: translateX(0) rotate(0deg); opacity: 1; } - 100% { transform: translateX(40px) rotate(8deg); opacity: 0; } -} - -.animate-swipe-left { - animation: swipeLeft 0.4s var(--ease-spring) forwards; -} - -.animate-swipe-right { - animation: swipeRight 0.4s var(--ease-spring) forwards; -} - -/* ── Modal animation ── */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideUp { - from { opacity: 0; transform: translateY(20px) scale(0.98); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.animate-fade-in { - animation: fadeIn 0.2s ease-out; -} - -.animate-slide-up { - animation: slideUp 0.3s var(--ease-spring); -} - -/* ── Subtle pulse for loading states ── */ -@keyframes subtlePulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } -} - -.animate-subtle-pulse { - animation: subtlePulse 2s ease-in-out infinite; -} - -/* ── Markdown content styles ── */ -.markdown-content h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; color: var(--color-text-heading); } -.markdown-content h2 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--color-text-heading); } -.markdown-content h3 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--color-text-heading); } -.markdown-content p { margin-bottom: 0.75rem; line-height: 1.7; } -.markdown-content ul { list-style: disc inside; padding-left: 0; margin-bottom: 0.75rem; text-align: inherit; } -.markdown-content ol { list-style: decimal inside; padding-left: 0; margin-bottom: 0.75rem; text-align: inherit; } -.markdown-content li { margin-bottom: 0.25rem; line-height: 1.6; } -.markdown-content strong { font-weight: 600; color: var(--color-text-heading); } -.markdown-content code { background: var(--color-bg-surface-alt); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875rem; } -.markdown-content table { border-collapse: collapse; width: 100%; margin-bottom: 0.75rem; } -.markdown-content th { background: var(--color-bg-surface-alt); font-weight: 600; text-align: left; padding: 0.5rem 0.75rem; border: 1px solid var(--color-border-light); } -.markdown-content td { padding: 0.5rem 0.75rem; border: 1px solid var(--color-border-light); } +.markdown-content h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: .5rem; color: var(--color-text-heading); } +.markdown-content h2 { font-size: 1.25rem; font-weight: 650; margin-bottom: .5rem; color: var(--color-text-heading); } +.markdown-content h3 { font-size: 1.125rem; font-weight: 650; margin-bottom: .5rem; color: var(--color-text-heading); } +.markdown-content p { margin-bottom: .75rem; line-height: 1.72; } +.markdown-content ul { list-style: disc; padding-left: 1.35rem; margin-bottom: .75rem; } +.markdown-content ol { list-style: decimal; padding-left: 1.35rem; margin-bottom: .75rem; } +.markdown-content li { margin-bottom: .25rem; line-height: 1.65; } +.markdown-content strong { font-weight: 700; color: var(--color-text-heading); } +.markdown-content code { background: var(--color-bg-surface-alt); padding: .15rem .4rem; border-radius: .35rem; font-size: .875rem; } +.markdown-content table { border-collapse: collapse; width: 100%; margin-bottom: .75rem; } +.markdown-content th { background: var(--color-bg-surface-alt); font-weight: 650; text-align: left; padding: .6rem .8rem; border: 1px solid var(--color-border-light); } +.markdown-content td { padding: .6rem .8rem; border: 1px solid var(--color-border-light); } .markdown-content *:last-child { margin-bottom: 0; } + +@media (max-width: 767px) { + .app-page { padding-top: 1.25rem; padding-bottom: 6rem; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d0e2e7a..f92c5fb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,16 +1,23 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Manrope, Newsreader } from "next/font/google"; +import Script from "next/script"; import "./globals.css"; -const inter = Inter({ - variable: "--font-inter", +const manrope = Manrope({ + variable: "--font-manrope", + subsets: ["latin"], + display: "swap", +}); + +const newsreader = Newsreader({ + variable: "--font-newsreader", subsets: ["latin"], display: "swap", }); export const metadata: Metadata = { - title: "Study App", - description: "Practice exams and quizzes", + title: "Study Desk", + description: "A focused workspace for flashcards and practice quizzes", }; export default function RootLayout({ @@ -19,23 +26,21 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - {children} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 0df09b9..30b8ab0 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; +import { ThemeToggle } from "@/components/ui/ThemeToggle"; export default function LoginPage() { const [password, setPassword] = useState(""); @@ -55,37 +56,28 @@ export default function LoginPage() { } return ( -
-
+
+
+
+
+
+
{/* Logo / Title area */} -
-
- - - -
-

- {isSetup ? "Welcome to Study App!" : "Study App"} +
+
S
+

Personal workspace

+

+ {isSetup ? "Welcome to Study Desk" : "Welcome back"}

{isSetup ? "Please set your admin password for the first time." - : "Enter your password to continue"} + : "Open your notes, decks, and practice quizzes."}

{/* Login card */} -
+
@@ -127,7 +119,7 @@ export default function LoginPage() { + Quizzes +
+ )} + + +
+
+ Appearance +
+
- + + ); + + return ( + <> + + +
+ + + Study Desk + + +
+ + {open && ( +
+ + {nav} + +
+ )} + ); } diff --git a/src/components/ui/ShareMenu.tsx b/src/components/ui/ShareMenu.tsx index 0349656..f24535c 100644 --- a/src/components/ui/ShareMenu.tsx +++ b/src/components/ui/ShareMenu.tsx @@ -45,8 +45,15 @@ export function ShareMenu({ targetType, contentId, classSlug }: ShareMenuProps) async function handleCopy() { if (!token) return; - const typeStr = targetType === "GROUP" ? "groups" : (targetType === "DECK" ? "flashcards" : "quizzes"); - const url = `${window.location.origin}/shared/${classSlug}/${typeStr}/${token}`; + const typeStr = isGroupShared + ? "groups" + : targetType === "GROUP" + ? "groups" + : targetType === "DECK" + ? "flashcards" + : "quizzes"; + const itemQuery = isGroupShared ? `?itemId=${encodeURIComponent(contentId)}` : ""; + const url = `${window.location.origin}/shared/${classSlug}/${typeStr}/${token}${itemQuery}`; await navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -56,7 +63,7 @@ export function ShareMenu({ targetType, contentId, classSlug }: ShareMenuProps)