Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/validate-content.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const CONTENT_DIR = path.join(__dirname, '..', 'src', 'content')
const REQUIRED_FIELDS = ['title', 'category', 'subcategory', 'tags', 'difficulty', 'lang']
const VALID_DIFFICULTIES = ['beginner', 'intermediate', 'advanced']
const VALID_DIFFICULTIES = ['junior', 'pleno', 'senior', 'especialista']
const VALID_LANGS = ['en', 'pt']
const REQUIRED_SECTIONS = ['## Full Answer', '## Quick Answer', '## Flashcard']

Expand Down
7 changes: 4 additions & 3 deletions src/app/[locale]/questions/[category]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export async function generateStaticParams() {
}

const difficultyVariant: Record<string, 'blue' | 'green' | 'orange'> = {
beginner: 'green',
intermediate: 'blue',
advanced: 'orange',
junior: 'green',
pleno: 'blue',
senior: 'orange',
especialista: 'orange',
}

function extractFlashcardParts(flashcardHtml: string): { front: string; back: string } {
Expand Down
153 changes: 153 additions & 0 deletions src/app/[locale]/updates/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { getTranslations, getLocale } from 'next-intl/server'

type ChangelogEntry = {
date: string
version: string
title: string
titlePt: string
points: string[]
pointsPt: string[]
}

const CHANGELOG: ChangelogEntry[] = [
{
date: '2026-06-03',
version: '1.4.0',
title: 'Filter redesign: seniority pills & tag search',
titlePt: 'Filtros redesenhados: pills de nível e busca de tags',
points: [
'Replaced flat tag button list with a search input — type to filter, click to add as chip',
'Added seniority level filter pills (Junior / Mid-level / Senior / Specialist) with multi-select',
'Filter logic: level AND tags — questions must match both active filters',
'Keyboard shortcuts: Enter selects top suggestion, Escape clears input, Backspace removes last chip',
'Clear all filters with one click',
],
pointsPt: [
'Lista gigante de botões de tag substituída por input de busca — digite para filtrar, clique para adicionar como chip',
'Filtro de nível de senioridade com pills multi-seleção (Júnior / Pleno / Sênior / Especialista)',
'Lógica combinada: nível E tags — questões aparecem quando atendem ambos os filtros ativos',
'Atalhos de teclado: Enter seleciona primeira sugestão, Escape limpa o input, Backspace remove último chip',
'Limpar todos os filtros com um clique',
],
},
{
date: '2026-06-03',
version: '1.3.0',
title: 'Seniority levels & Updates page',
titlePt: 'Níveis de senioridade e página de Atualizações',
points: [
'Replaced generic difficulty labels (beginner/intermediate/advanced) with job seniority levels (Junior/Mid-level/Senior/Specialist)',
'Added this Updates page to track project improvements over time',
],
pointsPt: [
'Rótulos genéricos de dificuldade (beginner/intermediate/advanced) substituídos por níveis de cargo (Júnior/Pleno/Sênior/Especialista)',
'Adicionada esta página de Atualizações para registrar melhorias ao longo do tempo',
],
},
{
date: '2026-05-26',
version: '1.2.0',
title: 'New questions & community contributions',
titlePt: 'Novas perguntas e contribuições da comunidade',
points: [
'Added Backend questions: Node.js middlewares',
'Added Frontend questions: web accessibility',
'Added DevOps questions: Docker, Kubernetes, containers vs VMs',
'Added Architecture questions: scalable software architecture',
],
pointsPt: [
'Novas perguntas de Backend: middlewares no Node.js',
'Novas perguntas de Frontend: acessibilidade web',
'Novas perguntas de DevOps: Docker, Kubernetes, containers vs VMs',
'Novas perguntas de Arquitetura: arquitetura de software escalável',
],
},
{
date: '2026-05-10',
version: '1.1.0',
title: 'Syntax highlighting, analytics & polish',
titlePt: 'Syntax highlighting, analytics e melhorias visuais',
points: [
'Integrated Shiki for syntax highlighting in code blocks',
'Added GitHub Flavored Markdown (GFM) support',
'Integrated Vercel Analytics',
'Updated favicon and app icons',
'Fixed GitHub contributors API authentication',
],
pointsPt: [
'Syntax highlighting em blocos de código com Shiki',
'Suporte a GitHub Flavored Markdown (GFM)',
'Analytics integrado via Vercel Analytics',
'Atualização de favicon e ícones do app',
'Correção na autenticação da API de contribuidores do GitHub',
],
},
{
date: '2026-05-01',
version: '1.0.0',
title: 'Initial launch',
titlePt: 'Lançamento inicial',
points: [
'Open-source interview preparation portal for developers',
'Questions in English and Portuguese',
'Full answer, quick answer, and interactive flashcard for each question',
'Study progress saved locally in the browser — no login required',
'Categories: Frontend, Backend, DevOps, Architecture, Security, Soft Skills',
],
pointsPt: [
'Portal open source de preparação para entrevistas para desenvolvedores',
'Perguntas em inglês e português',
'Resposta completa, resposta rápida e flashcard interativo para cada pergunta',
'Progresso de estudo salvo localmente no navegador — sem cadastro',
'Categorias: Frontend, Backend, DevOps, Arquitetura, Segurança, Soft Skills',
],
},
]

export default async function UpdatesPage() {
const t = await getTranslations('pages')
const locale = await getLocale()
const isPt = locale === 'pt'

return (
<div className="max-w-3xl mx-auto px-4 py-12">
<h1 className="font-mono text-3xl font-bold text-gray-900 dark:text-dark-heading mb-2">
{t('updates')}
</h1>
<p className="text-gray-500 dark:text-dark-muted mb-10">
{isPt
? 'O que foi melhorado, corrigido e adicionado ao longo do tempo.'
: 'What has been improved, fixed, and added over time.'}
</p>

<div className="flex flex-col gap-8">
{CHANGELOG.map((entry) => (
<div
key={entry.version}
className="bg-gray-50 dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg p-6"
>
<div className="flex flex-wrap items-center gap-3 mb-3">
<span className="font-mono text-xs bg-blue-500/10 text-blue-400 border border-blue-500/20 rounded px-2 py-0.5">
v{entry.version}
</span>
<span className="text-xs text-gray-400 dark:text-dark-muted font-mono">
{entry.date}
</span>
</div>
<h2 className="font-mono font-bold text-gray-900 dark:text-dark-heading text-lg mb-3">
{isPt ? entry.titlePt : entry.title}
</h2>
<ul className="flex flex-col gap-1.5">
{(isPt ? entry.pointsPt : entry.points).map((point, i) => (
<li key={i} className="flex gap-2 text-sm text-gray-600 dark:text-dark-text">
<span className="text-blue-400 mt-0.5 shrink-0">–</span>
<span>{point}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
)
}
8 changes: 8 additions & 0 deletions src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export function Footer() {
{nav("contribute")}
</Link>
</li>
<li>
<Link
href={`/${locale}/updates`}
className="text-sm text-dark-text hover:text-dark-heading transition-colors"
>
{nav("updates")}
</Link>
</li>
</ul>
</div>

Expand Down
12 changes: 7 additions & 5 deletions src/components/questions/QuestionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import Link from 'next/link'
import { useLocale } from 'next-intl'
import { useLocale, useTranslations } from 'next-intl'
import { useEffect, useState } from 'react'
import { getQuestionStatus } from '@/lib/progress'
import { Badge } from '@/components/ui/Badge'
Expand All @@ -12,13 +12,15 @@ type Props = {
}

const difficultyVariant: Record<string, 'blue' | 'green' | 'orange'> = {
beginner: 'green',
intermediate: 'blue',
advanced: 'orange',
junior: 'green',
pleno: 'blue',
senior: 'orange',
especialista: 'orange',
}

export function QuestionCard({ question }: Props) {
const locale = useLocale()
const t = useTranslations('question')
const [status, setStatus] = useState<'known' | 'review' | null>(null)

useEffect(() => {
Expand All @@ -37,7 +39,7 @@ export function QuestionCard({ question }: Props) {
</h3>
<div className="flex items-center gap-2 mt-2 flex-wrap">
<Badge variant={difficultyVariant[question.difficulty] ?? 'gray'}>
{question.difficulty}
{t(`difficulty.${question.difficulty}`)}
</Badge>
{question.tags.slice(0, 3).map(tag => (
<Badge key={tag} variant="gray">{tag}</Badge>
Expand Down
Loading
Loading