Reubencf commited on
Commit
71793e1
Β·
1 Parent(s): 5aa0d89

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
- <h2 className="mt-8 text-2xl sm:text-3xl font-black text-black tracking-tight uppercase leading-none">
10
- Enter a phrase to build a mini lesson
11
- </h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-2.5 text-xs">
12
  <div className="flex items-center gap-2.5">
13
- <dt className="w-10 shrink-0 text-[10px] font-black uppercase tracking-wider text-black bg-[#FFD21E] border border-black rounded py-0.5 text-center shadow-[1px_1px_0px_rgba(0,0,0,1)]">On</dt>
14
- <dd className="text-black font-bold">
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-10 shrink-0 text-[10px] font-black uppercase tracking-wider text-white bg-[#0EA5A4] border border-black rounded py-0.5 text-center shadow-[1px_1px_0px_rgba(0,0,0,1)]">Kun</dt>
20
- <dd className="text-black font-bold">
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-3 py-1.5 text-xs font-black text-black shadow-[2px_2px_0px_rgba(0,0,0,1)] has-furi">
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-base 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,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-2 text-sm font-bold text-gray-700">{word.translation}</div>
22
- {word.note ? <p className="mt-2 text-xs font-black text-gray-500">{word.note}</p> : null}
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, Sparkles } from "lucide-react";
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={<Sparkles size={22} />}>
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
- "rounded-full p-1.5 text-gray-400 transition-colors hover:bg-blue-50 hover:text-blue-600"
 
 
80
  }
81
  >
82
  {speaking ? (
83
- <Square size={15} className="fill-current" />
84
  ) : (
85
- <Volume2 size={15} />
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
  );