feat(phrases): progress bar while generating + bigger kanji/related text + brutalist TTS
Browse files- Replace empty-state headline with animated progress bar during generation
- Swap Sparkles icon for BookOpen on the Related words section
- Increase on/kunyomi readings and related word text to read more like a lesson
- Restyle TTSButton with neo-brutalist border/shadow (yellow idle, red speaking)
app/(app)/phrases/PhraseResult/EmptyState.tsx
CHANGED
|
@@ -1,14 +1,35 @@
|
|
| 1 |
import { PenLine } from "lucide-react";
|
| 2 |
|
| 3 |
-
export function EmptyState() {
|
| 4 |
return (
|
| 5 |
<div className="rounded-2xl bg-white p-8 text-center sm:p-12 border-3 border-black shadow-[8px_8px_0px_rgba(0,0,0,1)] relative overflow-hidden">
|
| 6 |
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-2xl bg-[#FFD21E] border-3 border-black text-black shadow-[4px_4px_0px_rgba(0,0,0,1)] rotate-3">
|
| 7 |
<PenLine size={32} strokeWidth={3} className="-rotate-3" />
|
| 8 |
</div>
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
</div>
|
| 13 |
);
|
| 14 |
}
|
|
|
|
| 1 |
import { PenLine } from "lucide-react";
|
| 2 |
|
| 3 |
+
export function EmptyState({ isPending = false }: { isPending?: boolean }) {
|
| 4 |
return (
|
| 5 |
<div className="rounded-2xl bg-white p-8 text-center sm:p-12 border-3 border-black shadow-[8px_8px_0px_rgba(0,0,0,1)] relative overflow-hidden">
|
| 6 |
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-2xl bg-[#FFD21E] border-3 border-black text-black shadow-[4px_4px_0px_rgba(0,0,0,1)] rotate-3">
|
| 7 |
<PenLine size={32} strokeWidth={3} className="-rotate-3" />
|
| 8 |
</div>
|
| 9 |
+
{isPending ? (
|
| 10 |
+
<div className="mt-8 space-y-4">
|
| 11 |
+
<div className="text-xl sm:text-2xl font-black text-black tracking-tight uppercase leading-none">
|
| 12 |
+
Building your mini lesson...
|
| 13 |
+
</div>
|
| 14 |
+
<div className="mx-auto h-6 w-full max-w-md overflow-hidden rounded-full border-3 border-black bg-white shadow-[4px_4px_0px_rgba(0,0,0,1)]">
|
| 15 |
+
<div className="phrases-progress-bar h-full bg-[#0EA5A4]" />
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
) : (
|
| 19 |
+
<h2 className="mt-8 text-2xl sm:text-3xl font-black text-black tracking-tight uppercase leading-none">
|
| 20 |
+
Enter a phrase to build a mini lesson
|
| 21 |
+
</h2>
|
| 22 |
+
)}
|
| 23 |
+
<style>{`
|
| 24 |
+
@keyframes phrases-progress-slide {
|
| 25 |
+
0% { transform: translateX(-100%); }
|
| 26 |
+
100% { transform: translateX(300%); }
|
| 27 |
+
}
|
| 28 |
+
.phrases-progress-bar {
|
| 29 |
+
width: 33%;
|
| 30 |
+
animation: phrases-progress-slide 1.4s ease-in-out infinite;
|
| 31 |
+
}
|
| 32 |
+
`}</style>
|
| 33 |
</div>
|
| 34 |
);
|
| 35 |
}
|
app/(app)/phrases/PhraseResult/KanjiCard.tsx
CHANGED
|
@@ -8,22 +8,22 @@ export function KanjiCard({ kanji }: { kanji: KanjiInfo }) {
|
|
| 8 |
<div className="text-4xl font-black leading-none text-black">{kanji.kanji}</div>
|
| 9 |
<div className="text-sm font-bold text-gray-700">{kanji.meaning}</div>
|
| 10 |
</div>
|
| 11 |
-
<dl className="mt-4 space-y-
|
| 12 |
<div className="flex items-center gap-2.5">
|
| 13 |
-
<dt className="w-
|
| 14 |
-
<dd className="text-black font-
|
| 15 |
{kanji.onyomi.length ? kanji.onyomi.join("γ") : <span className="text-gray-300">β</span>}
|
| 16 |
</dd>
|
| 17 |
</div>
|
| 18 |
<div className="flex items-center gap-2.5">
|
| 19 |
-
<dt className="w-
|
| 20 |
-
<dd className="text-black font-
|
| 21 |
{kanji.kunyomi.length ? kanji.kunyomi.join("γ") : <span className="text-gray-300">β</span>}
|
| 22 |
</dd>
|
| 23 |
</div>
|
| 24 |
</dl>
|
| 25 |
{kanji.exampleWord ? (
|
| 26 |
-
<div className="mt-4 inline-flex rounded-lg bg-[#E6FBFA] border-2 border-black px-
|
| 27 |
<Furi
|
| 28 |
text={kanji.exampleWord}
|
| 29 |
segments={
|
|
|
|
| 8 |
<div className="text-4xl font-black leading-none text-black">{kanji.kanji}</div>
|
| 9 |
<div className="text-sm font-bold text-gray-700">{kanji.meaning}</div>
|
| 10 |
</div>
|
| 11 |
+
<dl className="mt-4 space-y-3 text-base">
|
| 12 |
<div className="flex items-center gap-2.5">
|
| 13 |
+
<dt className="w-12 shrink-0 text-xs font-black uppercase tracking-wider text-black bg-[#FFD21E] border-2 border-black rounded py-1 text-center shadow-[2px_2px_0px_rgba(0,0,0,1)]">On</dt>
|
| 14 |
+
<dd className="text-black font-black text-lg">
|
| 15 |
{kanji.onyomi.length ? kanji.onyomi.join("γ") : <span className="text-gray-300">β</span>}
|
| 16 |
</dd>
|
| 17 |
</div>
|
| 18 |
<div className="flex items-center gap-2.5">
|
| 19 |
+
<dt className="w-12 shrink-0 text-xs font-black uppercase tracking-wider text-white bg-[#0EA5A4] border-2 border-black rounded py-1 text-center shadow-[2px_2px_0px_rgba(0,0,0,1)]">Kun</dt>
|
| 20 |
+
<dd className="text-black font-black text-lg">
|
| 21 |
{kanji.kunyomi.length ? kanji.kunyomi.join("γ") : <span className="text-gray-300">β</span>}
|
| 22 |
</dd>
|
| 23 |
</div>
|
| 24 |
</dl>
|
| 25 |
{kanji.exampleWord ? (
|
| 26 |
+
<div className="mt-4 inline-flex rounded-lg bg-[#E6FBFA] border-2 border-black px-4 py-2 text-base font-black text-black shadow-[3px_3px_0px_rgba(0,0,0,1)] has-furi">
|
| 27 |
<Furi
|
| 28 |
text={kanji.exampleWord}
|
| 29 |
segments={
|
app/(app)/phrases/PhraseResult/RelatedWordCard.tsx
CHANGED
|
@@ -6,7 +6,7 @@ export function RelatedWordCard({ word, lang }: { word: RelatedWord; lang: strin
|
|
| 6 |
return (
|
| 7 |
<div className="rounded-xl bg-white p-5 border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)]">
|
| 8 |
<div className="flex items-center justify-between gap-2">
|
| 9 |
-
<div className={`text-
|
| 10 |
<Furi text={word.word} segments={word.wordSegments} />
|
| 11 |
</div>
|
| 12 |
<div className="flex items-center gap-2">
|
|
@@ -18,8 +18,8 @@ export function RelatedWordCard({ word, lang }: { word: RelatedWord; lang: strin
|
|
| 18 |
<TTSButton text={word.word} lang={lang} />
|
| 19 |
</div>
|
| 20 |
</div>
|
| 21 |
-
<div className="mt-
|
| 22 |
-
{word.note ? <p className="mt-2 text-
|
| 23 |
</div>
|
| 24 |
);
|
| 25 |
}
|
|
|
|
| 6 |
return (
|
| 7 |
<div className="rounded-xl bg-white p-5 border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)]">
|
| 8 |
<div className="flex items-center justify-between gap-2">
|
| 9 |
+
<div className={`text-xl font-black text-black ${word.wordSegments ? "has-furi" : ""}`}>
|
| 10 |
<Furi text={word.word} segments={word.wordSegments} />
|
| 11 |
</div>
|
| 12 |
<div className="flex items-center gap-2">
|
|
|
|
| 18 |
<TTSButton text={word.word} lang={lang} />
|
| 19 |
</div>
|
| 20 |
</div>
|
| 21 |
+
<div className="mt-3 text-base font-bold text-gray-700">{word.translation}</div>
|
| 22 |
+
{word.note ? <p className="mt-2 text-sm font-black text-gray-500">{word.note}</p> : null}
|
| 23 |
</div>
|
| 24 |
);
|
| 25 |
}
|
app/(app)/phrases/PhraseResult/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { Languages, Lightbulb, ListChecks, MessageSquareText
|
| 2 |
import type { PhraseAnalysis } from "@/lib/hf/phrases";
|
| 3 |
import { Section } from "./Section";
|
| 4 |
import { HeaderCard } from "./HeaderCard";
|
|
@@ -36,7 +36,7 @@ export function PhraseResult({ result, lang }: { result: PhraseAnalysis; lang: s
|
|
| 36 |
) : null}
|
| 37 |
|
| 38 |
{result.relatedWords.length > 0 ? (
|
| 39 |
-
<Section title="Related words" icon={<
|
| 40 |
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
| 41 |
{result.relatedWords.map((word, index) => (
|
| 42 |
<RelatedWordCard key={`${word.word}-${index}`} word={word} lang={lang} />
|
|
|
|
| 1 |
+
import { BookOpen, Languages, Lightbulb, ListChecks, MessageSquareText } from "lucide-react";
|
| 2 |
import type { PhraseAnalysis } from "@/lib/hf/phrases";
|
| 3 |
import { Section } from "./Section";
|
| 4 |
import { HeaderCard } from "./HeaderCard";
|
|
|
|
| 36 |
) : null}
|
| 37 |
|
| 38 |
{result.relatedWords.length > 0 ? (
|
| 39 |
+
<Section title="Related words" icon={<BookOpen size={22} strokeWidth={2.5} />}>
|
| 40 |
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
| 41 |
{result.relatedWords.map((word, index) => (
|
| 42 |
<RelatedWordCard key={`${word.word}-${index}`} word={word} lang={lang} />
|
app/(app)/phrases/PhrasesClient.tsx
CHANGED
|
@@ -94,7 +94,7 @@ export function PhrasesClient({ defaultLevel, lang }: { defaultLevel: Level; lan
|
|
| 94 |
) : null}
|
| 95 |
</div>
|
| 96 |
|
| 97 |
-
{result ? <PhraseResult result={result} lang={lang} /> : <EmptyState />}
|
| 98 |
</div>
|
| 99 |
);
|
| 100 |
}
|
|
|
|
| 94 |
) : null}
|
| 95 |
</div>
|
| 96 |
|
| 97 |
+
{isPending ? <EmptyState isPending /> : result ? <PhraseResult result={result} lang={lang} /> : <EmptyState />}
|
| 98 |
</div>
|
| 99 |
);
|
| 100 |
}
|
components/TTSButton.tsx
CHANGED
|
@@ -76,13 +76,15 @@ export function TTSButton({
|
|
| 76 |
aria-label={speaking ? "Stop speaking" : "Listen"}
|
| 77 |
className={
|
| 78 |
className ??
|
| 79 |
-
|
|
|
|
|
|
|
| 80 |
}
|
| 81 |
>
|
| 82 |
{speaking ? (
|
| 83 |
-
<Square size={
|
| 84 |
) : (
|
| 85 |
-
<Volume2 size={
|
| 86 |
)}
|
| 87 |
</button>
|
| 88 |
);
|
|
|
|
| 76 |
aria-label={speaking ? "Stop speaking" : "Listen"}
|
| 77 |
className={
|
| 78 |
className ??
|
| 79 |
+
`rounded-lg border-2 border-black p-1.5 text-black shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[3px_3px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-[1px_1px_0px_rgba(0,0,0,1)] transition-all ${
|
| 80 |
+
speaking ? "bg-[#FF8080]" : "bg-[#FFD21E]"
|
| 81 |
+
}`
|
| 82 |
}
|
| 83 |
>
|
| 84 |
{speaking ? (
|
| 85 |
+
<Square size={16} strokeWidth={3} className="fill-current" />
|
| 86 |
) : (
|
| 87 |
+
<Volume2 size={16} strokeWidth={3} />
|
| 88 |
)}
|
| 89 |
</button>
|
| 90 |
);
|