Terminology (lib/i18n-dict.ts) - One name per mode everywhere: "Study Deck" / "Studijní balíček" and "Homework Solver" / "Řešitel úkolů". The library empty-state cards and the /create switcher now read the same, so the choice no longer changes name between screens. Deep links - Library quick actions carry the mode: /create?mode=deck and /homework?mode=homework. - Both pages canonicalize: /create?mode=homework redirects to the homework flow and /homework?mode=deck redirects to /create, so a mode link always opens that mode directly. /create - The two bulky square cards (106px tall, wrapping/clipping "Study Deck & Qu…" on phones) are replaced by a compact segmented control (52px) placed directly above "1. Add your notes": ice pill, snowflake/sparkles icons, 44px segments and one-line labels at 360px. The pill slides to the chosen segment and the route follows ~190ms later, so toggling reads as one smooth transition (immediate under prefers-reduced-motion). Service worker: freezmo-v9. Verified: mode-switch suite 26/26 (deep links, cross-mode redirects, 52px control, 44px segments, no clipping/wrapping at 360px, "1. Add your notes" at y=394 of 740 with the upload controls above the fold, pill slide, axe contrast, en+cs, phone+desktop), mobile 49/49, i18n 27/27, desktop regression 73/73. Lint + build clean.
80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
import { SnowflakeIcon, SparklesIcon } from "@/components/icons";
|
|
import { useI18n } from "@/lib/i18n";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export type StudyMode = "deck" | "homework";
|
|
|
|
/** Canonical URL per mode, so a deep link always lands on the right screen. */
|
|
export const MODE_HREF: Record<StudyMode, string> = {
|
|
deck: "/create?mode=deck",
|
|
homework: "/homework?mode=homework",
|
|
};
|
|
|
|
/** How long the ice pill takes to slide before the route follows. */
|
|
const SLIDE_MS = 180;
|
|
|
|
/**
|
|
* Compact two-segment mode switch (Study Deck / Homework Solver). The ice pill
|
|
* slides to the chosen segment and the router follows a moment later, so the
|
|
* toggle reads as one smooth transition instead of a hard jump. The current
|
|
* mode stays pressed; the other segment navigates.
|
|
*/
|
|
export function ModeSwitch({ current }: { current: StudyMode }) {
|
|
const { t } = useI18n();
|
|
const router = useRouter();
|
|
const [pending, setPending] = useState<StudyMode | null>(null);
|
|
const timer = useRef<number | null>(null);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const choose = (mode: StudyMode) => {
|
|
if (mode === current || pending !== null) return;
|
|
setPending(mode);
|
|
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
timer.current = window.setTimeout(() => router.push(MODE_HREF[mode]), reduce ? 0 : SLIDE_MS + 10);
|
|
};
|
|
|
|
const shown = pending ?? current;
|
|
|
|
const segment = (mode: StudyMode, icon: React.ReactNode, label: string) => (
|
|
<button
|
|
type="button"
|
|
aria-pressed={current === mode}
|
|
onClick={() => choose(mode)}
|
|
className={cn(
|
|
"relative z-10 inline-flex min-h-11 items-center justify-center gap-1.5 rounded-xl px-2 text-[13.5px] font-semibold transition-colors",
|
|
shown === mode ? "text-[#06111f]" : "text-frost-700 hover:text-[var(--text)] dark:text-frost-300",
|
|
)}
|
|
>
|
|
{icon}
|
|
<span className="whitespace-nowrap">{label}</span>
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<div
|
|
data-testid="mode-switch"
|
|
className="relative grid grid-cols-2 rounded-[0.95rem] border border-[var(--hairline)] bg-[var(--surface-sunken)] p-[3px] backdrop-blur-sm"
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className={cn(
|
|
"grad-ice pointer-events-none absolute inset-y-[3px] left-[3px] w-[calc(50%-3px)] rounded-xl shadow-[0_6px_16px_-8px_rgb(56_189_248/0.7)] transition-transform duration-[180ms] ease-out motion-reduce:transition-none",
|
|
shown === "homework" && "translate-x-full",
|
|
)}
|
|
/>
|
|
{segment("deck", <SnowflakeIcon className="size-4" />, t("actionStudyDeck"))}
|
|
{segment("homework", <SparklesIcon className="size-4" />, t("actionHomework"))}
|
|
</div>
|
|
);
|
|
}
|