Skip to content

Commit c4bd6f8

Browse files
Merge pull request #352 from DevLoversTeam/feat/qa-empty-state-polish
(SP: 2) [Frontend] Q&A Empty State UX Refresh
2 parents 2532b26 + a2c9702 commit c4bd6f8

9 files changed

Lines changed: 109 additions & 18 deletions

File tree

frontend/app/globals.css

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,24 @@ html {
183183
.qa-accordion-item:hover,
184184
.qa-accordion-item:focus-within,
185185
.qa-accordion-item[data-state='open'] {
186-
border-color: var(--qa-accent);
186+
border-color: var(--qa-accent, var(--accent-primary));
187+
}
188+
189+
.qa-accordion-item {
190+
position: relative;
191+
overflow: hidden;
192+
background-image: linear-gradient(
193+
90deg,
194+
transparent 0%,
195+
transparent 54%,
196+
var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100%
197+
);
198+
}
199+
200+
.qa-accordion-item:hover,
201+
.qa-accordion-item:focus-within,
202+
.qa-accordion-item[data-state='open'] {
203+
background-image: none;
187204
}
188205

189206
.no-select,
@@ -195,6 +212,31 @@ html {
195212
user-select: none;
196213
}
197214

215+
@keyframes fade-up {
216+
from {
217+
transform: translateY(8px);
218+
opacity: 0;
219+
}
220+
221+
to {
222+
transform: translateY(0);
223+
opacity: 1;
224+
}
225+
}
226+
227+
.animate-fade-up {
228+
animation: fade-up 0.6s ease-out forwards;
229+
opacity: 0;
230+
}
231+
232+
.delay-150 {
233+
animation-delay: 0.15s;
234+
}
235+
236+
.delay-300 {
237+
animation-delay: 0.3s;
238+
}
239+
198240
@keyframes progress {
199241
from {
200242
width: 100%;
@@ -535,4 +577,4 @@ html {
535577
.animate-dash-flow {
536578
animation: none !important;
537579
}
538-
}
580+
}

frontend/components/q&a/AccordionList.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ import {
3636
import { categoryTabStyles } from '@/data/categoryStyles';
3737
import { CACHE_KEY, getCachedTerms } from '@/lib/ai/explainCache';
3838

39+
type QaItemStyle = CSSProperties & {
40+
'--qa-accent': string;
41+
'--qa-accent-soft': string;
42+
};
43+
44+
function normalizeCachedTerm(term: string): string {
45+
return term.toLowerCase().trim();
46+
}
47+
48+
function hexToRgba(hex: string, alpha: number): string {
49+
const normalized = hex.replace('#', '');
50+
if (normalized.length !== 6) return `rgba(0, 0, 0, ${alpha})`;
51+
const r = parseInt(normalized.slice(0, 2), 16);
52+
const g = parseInt(normalized.slice(2, 4), 16);
53+
const b = parseInt(normalized.slice(4, 6), 16);
54+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
55+
}
56+
3957
function isListItemBlock(value: ListEntry): value is ListItemBlock {
4058
return (
4159
!!value &&
@@ -307,11 +325,11 @@ export default function AccordionList({ items }: { items: QuestionEntry[] }) {
307325
);
308326
const [modalOpen, setModalOpen] = useState(false);
309327
const [cachedTerms, setCachedTerms] = useState<Set<string>>(
310-
() => new Set(getCachedTerms())
328+
() => new Set(getCachedTerms().map(normalizeCachedTerm))
311329
);
312330

313331
const refreshCachedTerms = useCallback(() => {
314-
const terms = getCachedTerms();
332+
const terms = getCachedTerms().map(normalizeCachedTerm);
315333
setCachedTerms(new Set(terms));
316334
}, []);
317335

@@ -380,20 +398,21 @@ export default function AccordionList({ items }: { items: QuestionEntry[] }) {
380398
<Accordion type="single" collapsible className="w-full">
381399
{items.map((q, idx) => {
382400
const key = q.id ?? idx;
383-
const accent =
401+
const accentColor =
384402
categoryTabStyles[q.category as keyof typeof categoryTabStyles]
385-
?.accent;
403+
?.accent ?? '#A1A1AA';
386404
const animationDelay = `${Math.min(idx, 10) * 60}ms`;
387-
const itemStyle: CSSProperties = {
405+
const itemStyle: QaItemStyle = {
388406
animationDelay,
389407
animationFillMode: 'both',
390-
...(accent ? ({ '--qa-accent': accent } as CSSProperties) : {}),
408+
'--qa-accent': accentColor,
409+
'--qa-accent-soft': hexToRgba(accentColor, 0.22),
391410
};
392411
return (
393412
<AccordionItem
394413
key={key}
395414
value={String(key)}
396-
className="qa-accordion-item mb-3 rounded-xl border border-black/5 bg-white/90 shadow-sm transition-colors last:mb-0 last:border-b dark:border-white/10 dark:bg-neutral-900/80 animate-in fade-in slide-in-from-bottom-2 duration-500"
415+
className="qa-accordion-item mb-3 rounded-xl border border-black/5 bg-white/90 shadow-sm transition-colors last:mb-0 last:border-b dark:border-white/10 dark:bg-neutral-900/80 animate-in fade-in slide-in-from-bottom-2 duration-500 motion-reduce:animate-none"
397416
style={itemStyle}
398417
>
399418
<AccordionTrigger

frontend/components/q&a/FloatingExplainButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default function FloatingExplainButton({
5151

5252
return (
5353
<button
54+
type="button"
5455
ref={buttonRef}
5556
onClick={e => {
5657
e.stopPropagation();
@@ -67,6 +68,7 @@ export default function FloatingExplainButton({
6768
'hover:bg-(--accent-hover)',
6869
'transition-all duration-200',
6970
'animate-in fade-in-0 zoom-in-95',
71+
'motion-reduce:animate-none motion-reduce:transition-none',
7072
'focus:ring-2 focus:ring-(--accent-primary) focus:ring-offset-2 focus:outline-none'
7173
)}
7274
style={{

frontend/components/q&a/QaSection.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ export default function TabsSection() {
3535
() => `qa-${active}-${currentPage}`,
3636
[active, currentPage]
3737
);
38+
const emptyStateLines = useMemo(
39+
() =>
40+
t('noQuestions')
41+
.split('\n')
42+
.map(line => line.trim())
43+
.filter(Boolean),
44+
[t]
45+
);
3846

3947
const clearSelection = useCallback(() => {
4048
if (typeof window === 'undefined') return;
@@ -109,7 +117,23 @@ export default function TabsSection() {
109117
{items.length ? (
110118
<AccordionList key={animationKey} items={items} />
111119
) : (
112-
<p className="py-12 text-center">{t('noQuestions')}</p>
120+
<div className="py-20 text-center">
121+
{emptyStateLines[0] && (
122+
<p className="text-lg font-semibold text-gray-900 motion-safe:animate-fade-up motion-reduce:opacity-100 dark:text-white">
123+
{emptyStateLines[0]}
124+
</p>
125+
)}
126+
{emptyStateLines[1] && (
127+
<p className="mt-2 text-gray-400 motion-safe:animate-fade-up motion-safe:[animation-delay:150ms] motion-reduce:opacity-100 dark:text-gray-300">
128+
{emptyStateLines[1]}
129+
</p>
130+
)}
131+
{emptyStateLines[2] && (
132+
<p className="mt-1 text-gray-500 motion-safe:animate-fade-up motion-safe:[animation-delay:300ms] motion-reduce:opacity-100 dark:text-gray-400">
133+
{emptyStateLines[2]}
134+
</p>
135+
)}
136+
</div>
113137
)}
114138
</div>
115139
</TabsContent>

frontend/components/tests/q&a/qa-section.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('QaSection', () => {
6060

6161
it('renders category tabs and pagination', () => {
6262
qaState.totalPages = 3;
63+
qaState.items = [{ id: 'q1' }];
6364
render(<QaSection />);
6465

6566
const buttons = screen.getAllByRole('button');

frontend/components/ui/accordion.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ function AccordionTrigger({
4141
{...props}
4242
>
4343
{children}
44-
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
44+
<ChevronDownIcon
45+
aria-hidden="true"
46+
className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
47+
/>
4548
</AccordionPrimitive.Trigger>
4649
</AccordionPrimitive.Header>
4750
);
@@ -55,7 +58,7 @@ function AccordionContent({
5558
return (
5659
<AccordionPrimitive.Content
5760
data-slot="accordion-content"
58-
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
61+
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down motion-reduce:!animate-none overflow-hidden text-sm"
5962
{...props}
6063
>
6164
<div className={cn('pt-0 pb-4', className)}>{children}</div>

frontend/messages/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"searchPlaceholder": "Search questions...",
125125
"clearSearch": "Clear search",
126126
"noResults": "Nothing found for \"{query}\"",
127-
"noQuestions": "No questions found",
127+
"noQuestions": "No questions yet…\nBut they’re already in development.\nStay tuned - new content coming soon!",
128128
"metaTitle": "Q&A | DevLovers",
129129
"metaDescription": "Questions and answers for technical interview preparation",
130130
"frontend": "Frontend",
@@ -1477,4 +1477,4 @@
14771477
}
14781478
}
14791479
}
1480-
}
1480+
}

frontend/messages/pl.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"searchPlaceholder": "Szukaj pytań...",
125125
"clearSearch": "Wyczyść wyszukiwanie",
126126
"noResults": "Nic nie znaleziono dla \"{query}\"",
127-
"noQuestions": "Nie znaleziono pytań",
127+
"noQuestions": "Brak pytań na ten moment…\nAle już są w przygotowaniu.\nBądź na bieżąco - wkrótce pojawią się nowe materiały!",
128128
"metaTitle": "Q&A | DevLovers",
129129
"metaDescription": "Pytania i odpowiedzi do przygotowania do rozmów kwalifikacyjnych",
130130
"frontend": "Frontend",
@@ -1478,4 +1478,4 @@
14781478
}
14791479
}
14801480
}
1481-
}
1481+
}

frontend/messages/uk.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"searchPlaceholder": "Пошук...",
125125
"clearSearch": "Очистити пошук",
126126
"noResults": "Нічого не знайдено за запитом \"{query}\"",
127-
"noQuestions": "Питань не знайдено",
127+
"noQuestions": "Питань поки що немає…\nАле вони вже в розробці.\nСлідкуйте за оновленнями - скоро зʼявиться новий контент!",
128128
"metaTitle": "Q&A | DevLovers",
129129
"metaDescription": "Питання та відповіді для підготовки до технічних співбесід",
130130
"frontend": "Frontend",
@@ -1481,4 +1481,4 @@
14811481
}
14821482
}
14831483
}
1484-
}
1484+
}

0 commit comments

Comments
 (0)