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
69 changes: 69 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,72 @@

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

# Algoria Project Rules & Design System

This document outlines the design system, architecture rules, and coding standards for the Algoria project. All AI agents working on this codebase must adhere strictly to these guidelines.

## 1. Design System & Aesthetics

### Typography
- **Primary Font**: Geist Sans (sans-serif) for body text and general interface.
- **Monospace Font**: Geist Mono / system monospace for code, metrics, and technical data.
- **Digital LCD Font**: `Share Tech Mono` (imported from Google Fonts). Used for stopwatch numbers and digital display readouts (`.font-digital`).
- **Sci-Fi Display Font**: `Orbitron` (imported from Google Fonts). Used for headers and technical badges (`.font-orbitron`).

### Border Radius & Shapes
- **Zero Rounding Rule**: **DO NOT use rounded corners on any UI elements.** All buttons, inputs, dialogs, cards, progress bars, and badges must have sharp, square corners.
- Use `rounded-none` or omit the `rounded` utility class entirely.
- Avoid `rounded-sm`, `rounded-md`, `rounded-lg`, etc.

### Colors & Themes
- **Dark Mode (Default)**:
- Background: `#020617` (Deep space slate/black)
- Foreground: `#f8fafc`
- Secondary/Cards: `#0f172a`
- Border: `#1e293b`
- **Light Mode**:
- Background: `#ffffff`
- Foreground: `#020617`
- Secondary/Cards: `#f1f5f9`
- Border: `#e2e8f0`
- **Brand Colors**:
- **Primary**: Indigo (`#4f46e5` for light, `#818cf8` for dark)
- **Gamification Accent**: Orange/Amber (`#f97316` / `#f59e0b`) for active rewards and challenges.
- **Success/Complete Accent**: Emerald/Green (`#10b981` / `#22c55e`) for completed items.

### Animations & Motion
- **No Idle Pulsing**: Avoid looping animations such as `animate-pulse` or `animate-ping` on active status badges or pills to prevent visual clutter.
- **Transitions**: Keep page and state transitions subtle using Framer Motion (e.g. slight Y-axis slide or opacity transitions).

---

## 2. Architecture & Code Structure

### Clean Separation of Concerns
- **Logic vs View**: Do not bundle complex states, timers, event listeners, or storage reads/writes directly in UI view components.
- Move state, timing, and progress checks to **custom React hooks** (e.g., `useDailyChallenge`).
- Move storage keys, static constants, formatting helpers, and pure functions to **utility files** (e.g., `daily-challenge-utils.ts`).
- Keep `.tsx` UI files focused purely on layout, styling, and rendering.

---

## 3. Product Rules & Features

### Authentication & Gating
- **Full Gating**: The user must be logged in to view any primary problem solving content or dashboard data. Use `RequireAuth` to wrap protected views.
- **Auth Dialog**: Trigger the global `AuthDialog` via `AuthDialogProvider` to prompt user login/registration when trying to view protected pages, and redirect the user to their target pathname upon successful authentication.
- **Account Deletion**: When a user profile is deleted:
- Delete all records and progress associated with that user from the database.
- Perform `localStorage.clear()` on the client side to wipe all local progress caches.

### Daily Challenge Mechanic
- **Timer Details**:
- Requires **3 minutes (180 seconds)** of active page visibility to complete.
- Must pause automatically if the user switches browser tabs (`document.hidden`). Use sub-second (50ms) intervals to render a fluid stopwatch (`HH:MM:SS:CC`) in the UI.
- **Checklist Requirements**:
- Daily challenges are completed only if:
1. The problem is unlocked/accessible for the user.
2. The statement page is visited.
3. Both optimal and brute-force solutions (if both exist) have been visited.
4. The 3-minute active timer has finished.
1 change: 0 additions & 1 deletion CLAUDE.md

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Algoria

![](https://img.shields.io/badge/Versão-1.5.0-black?style=for-the-badge)
![](https://img.shields.io/badge/Versão-1.6.0-black?style=for-the-badge)

Plataforma em português para estudar **algoritmos e decisões em código** através de leitura guiada: catálogo de problemas com várias soluções (brute-force, óptima, alternativa), **code player** linha-a-linha com três níveis de explicação, mini-guias em **Conceitos**, **curso modular** com avaliações locais, hub de **inglês técnico para entrevistas** (conteúdo em inglês) e guias de **engenharia aplicada** (front, back, DevOps, SoftSkills e IA).

Expand Down
25 changes: 25 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Notas de Release (v1.6.0)

### 🚀 Novas Funcionalidades
- **Controle de Autenticação Estrito (Full Gating & Auth Dialog)**: Implementado o bloqueio completo de qualquer conteúdo de estudo (problemas, soluções comentadas, conceitos e guias de carreira) para utilizadores não autenticados. O acesso é interceptado reativamente através do componente `RequireAuth` que ativa o `AuthDialog` global e redireciona automaticamente o utilizador para a página pretendida após o sucesso do login.
- **Exclusão de Conta com Limpeza Total (Wipe)**: Criada uma caixa de diálogo de confirmação para a exclusão de conta no perfil do utilizador. A confirmação remove definitivamente todos os registros e históricos de progresso do banco de dados (Postgres) e limpa completamente o cache local do cliente através de `localStorage.clear()`.
- **Desafio Diário Aprimorado (Checklist e Validação)**: O sistema de Desafio Diário agora exige a validação estrita de três tarefas antes de considerar o desafio completo: 1) o problema deve ser acessível ao utilizador, 2) o enunciado deve ser visitado, 3) todas as soluções disponíveis do problema devem ser lidas, e 4) o utilizador deve passar pelo menos 3 minutos (180 segundos) de tempo ativo na página.

### 🛠️ Melhorias Técnicas & UI
- **Cronómetro Digital LCD/LED (Glowing Clock)**: Adicionado um widget de cronómetro baseado no modelo digital de sete segmentos (`HH:MM:SS:CC`) com renderização em tempo real atualizada a cada 50ms para exibição fluida de centésimos de segundo. O contador usa uma caixa preta de alto contraste com fontes personalizadas e sombras de texto luminescentes (*glow*) brancas e verdes.
- **Estrutura Modular (Separação de Conceitos)**: O rastreador do desafio foi dividido em três partes organizadas: os utilitários de armazenamento e formatação (`daily-challenge-utils.ts`), o hook de controle de efeitos e tempo ativo (`use-daily-challenge.ts`) e o componente de interface pura (`daily-challenge-tracker.tsx`).
- **Alinhamento com o Design System Estrito**: Removidas todas as bordas arredondadas da interface do banner (`rounded-none` absoluto) e limpos os efeitos de pulsação ociosa (`animate-pulse` / `animate-ping`) para garantir sobriedade visual e consistência técnica.
- **Layout Inteligente Flutuante (Sticky)**: O banner agora acompanha o scroll da página de forma fixa (`sticky top-16`) mantendo exatamente a mesma largura útil do conteúdo principal (`max-w-7xl px-6`) com efeito translúcido no fundo e zero impacto na tela quando inativo.

---

# Changelog

## 2026-05-29 — Strict Auth Gating, Account Wipe, and High-Fidelity Stopwatch Tracker
Esta versão foca-se na segurança de dados, privacidade do utilizador e fidelidade visual da interface de desafios, trazendo alinhamentos estritos com o design system da plataforma (zero cantos arredondados) e melhorias na estrutura do código de gamificação.

- **Controle de Acesso Total**: Gating estrito de todo o conteúdo educativo da plataforma, exigindo autenticação via modal global integrado antes do redirecionamento ao destino.
- **Apagamento Completo de Dados**: Funcionalidade de exclusão de conta no perfil que limpa as tabelas relacionais em nuvem e esvazia o armazenamento local do navegador em um único passo.
- **Cronómetro Centesimal com Glow**: Novo design de cronómetro digital com efeito neon de 8 dígitos, medindo o tempo de engajamento do utilizador com suporte a pausas de visibilidade da aba do browser.
- **Status Dashboard Monospace**: Exibição simplificada de requisitos em formato de terminal técnico, oferecendo maior clareza visual sobre as tarefas cumpridas (acesso, leitura e soluções visitadas) sem poluição por animações.
- **Arquitetura Modular em React**: Separação de utilitários de storage e custom hooks de timer, simplificando o componente de visualização.
71 changes: 2 additions & 69 deletions app/auth/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import Link from 'next/link';
import type { Metadata } from 'next';
import { ArrowLeft } from 'lucide-react';

import { SignInForm } from '@/components/auth/sign-in-form';
import { AlgoriaMark } from '@/components/branding/algoria-logo';
import { Button } from '@/components/ui/button';
import { SignInRedirector } from '@/components/auth/sign-in-redirector';
import { buildPublicMetadata } from '@/lib/seo/build-metadata';

export const metadata: Metadata = buildPublicMetadata({
Expand All @@ -15,68 +11,5 @@ export const metadata: Metadata = buildPublicMetadata({
});

export default function SignInPage() {
return (
<div className="relative flex-1 bg-grid-pattern">
{/* Decorative gradients */}
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
<div className="absolute -left-40 -top-40 h-80 w-80 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute -bottom-40 -right-40 h-80 w-80 rounded-full bg-primary/8 blur-3xl" />
</div>

<div className="relative z-10 mx-auto max-w-7xl px-6 py-16">
<Button asChild variant="outline" size="sm" className="mb-10 rounded-none gap-2 text-xs font-bold uppercase tracking-wide">
<Link href="/"><ArrowLeft className="h-3.5 w-3.5" /> Início</Link>
</Button>

<div className="grid gap-16 lg:grid-cols-[1fr_minmax(0,480px)]">
{/* Left — branding + value props */}
<div className="flex flex-col justify-center">
<div className="border-l-4 border-primary pl-8">
<p className="text-[10px] font-black uppercase tracking-[0.35em] text-primary">Conta</p>
<h1 className="mt-3 text-4xl font-black uppercase tracking-tight lg:text-5xl">
Iniciar sessão
</h1>
<p className="mt-4 max-w-lg text-base leading-relaxed text-muted-foreground">
Sincroniza o teu progresso, acede a todo o conteúdo Pro e retoma onde paraste.
</p>
</div>

<div className="mt-12 hidden lg:block">
<div className="space-y-6">
{[
{ label: 'Progresso na nuvem', desc: 'Continua o estudo em qualquer dispositivo sem perder o histórico.' },
{ label: 'Catálogo completo', desc: 'Acesso a todos os problemas, soluções comentadas e code player.' },
{ label: 'Curso guiado', desc: 'Módulos com avaliações e certificado gravado no browser.' },
].map((item) => (
<div key={item.label} className="flex items-start gap-4">
<span className="mt-1 flex h-8 w-8 shrink-0 items-center justify-center border border-primary/30 bg-primary/5 text-primary">
<span className="h-2 w-2 bg-primary" aria-hidden />
</span>
<div>
<p className="text-xs font-bold uppercase tracking-[0.1em] text-foreground">{item.label}</p>
<p className="mt-1 text-sm text-muted-foreground">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
</div>

{/* Right — form card */}
<div className="flex flex-col justify-center">
<div className="border-2 border-border/80 bg-background/80 shadow-2xl shadow-primary/5 backdrop-blur-xl">
<div className="h-1 bg-gradient-to-r from-primary via-primary/70 to-primary/30" aria-hidden />
<div className="px-8 pb-10 pt-8">
<div className="mb-6 flex items-center gap-3">
<AlgoriaMark className="h-8 w-8" />
<span className="text-lg font-black uppercase tracking-[0.12em]">Algoria</span>
</div>
<SignInForm />
</div>
</div>
</div>
</div>
</div>
</div>
);
return <SignInRedirector />;
}
71 changes: 2 additions & 69 deletions app/auth/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import Link from 'next/link';
import type { Metadata } from 'next';
import { ArrowLeft } from 'lucide-react';

import { SignUpForm } from '@/components/auth/sign-up-form';
import { AlgoriaMark } from '@/components/branding/algoria-logo';
import { Button } from '@/components/ui/button';
import { SignUpRedirector } from '@/components/auth/sign-up-redirector';
import { buildPublicMetadata } from '@/lib/seo/build-metadata';

export const metadata: Metadata = buildPublicMetadata({
Expand All @@ -15,68 +11,5 @@ export const metadata: Metadata = buildPublicMetadata({
});

export default function SignUpPage() {
return (
<div className="relative flex-1 bg-grid-pattern">
{/* Decorative gradients */}
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden>
<div className="absolute -right-40 -top-40 h-80 w-80 rounded-full bg-primary/8 blur-3xl" />
<div className="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-primary/5 blur-3xl" />
</div>

<div className="relative z-10 mx-auto max-w-7xl px-6 py-16">
<Button asChild variant="outline" size="sm" className="mb-10 rounded-none gap-2 text-xs font-bold uppercase tracking-wide">
<Link href="/"><ArrowLeft className="h-3.5 w-3.5" /> Início</Link>
</Button>

<div className="grid gap-16 lg:grid-cols-[1fr_minmax(0,480px)]">
{/* Left — branding + value props */}
<div className="flex flex-col justify-center">
<div className="border-l-4 border-primary pl-8">
<p className="text-[10px] font-black uppercase tracking-[0.35em] text-primary">Nova conta</p>
<h1 className="mt-3 text-4xl font-black uppercase tracking-tight lg:text-5xl">
Criar conta
</h1>
<p className="mt-4 max-w-lg text-base leading-relaxed text-muted-foreground">
Cria a tua conta gratuita e começa a estudar algoritmos com leitura guiada de código.
</p>
</div>

<div className="mt-12 hidden lg:block">
<div className="space-y-6">
{[
{ label: 'Gratuito para começar', desc: 'Acesso a problemas free, conceitos, inglês técnico e guias de engenharia.' },
{ label: 'Progresso sincronizado', desc: 'O teu progresso local será fundido com a conta no primeiro login.' },
{ label: 'Upgrade quando quiseres', desc: 'Plano Pro desbloqueia o catálogo completo de soluções e o code player.' },
].map((item) => (
<div key={item.label} className="flex items-start gap-4">
<span className="mt-1 flex h-8 w-8 shrink-0 items-center justify-center border border-primary/30 bg-primary/5 text-primary">
<span className="h-2 w-2 bg-primary" aria-hidden />
</span>
<div>
<p className="text-xs font-bold uppercase tracking-[0.1em] text-foreground">{item.label}</p>
<p className="mt-1 text-sm text-muted-foreground">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
</div>

{/* Right — form card */}
<div className="flex flex-col justify-center">
<div className="border-2 border-border/80 bg-background/80 shadow-2xl shadow-primary/5 backdrop-blur-xl">
<div className="h-1 bg-gradient-to-r from-primary/30 via-primary/70 to-primary" aria-hidden />
<div className="px-8 pb-10 pt-8">
<div className="mb-6 flex items-center gap-3">
<AlgoriaMark className="h-8 w-8" />
<span className="text-lg font-black uppercase tracking-[0.12em]">Algoria</span>
</div>
<SignUpForm />
</div>
</div>
</div>
</div>
</div>
</div>
);
return <SignUpRedirector />;
}
19 changes: 11 additions & 8 deletions app/concepts/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import Link from 'next/link';
import { ArrowLeft, Clock } from 'lucide-react';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { headers } from 'next/headers';
import { ArrowLeft, Clock } from 'lucide-react';
import Link from 'next/link';
import { notFound } from 'next/navigation';

import { RequireAuth } from '@/components/auth/require-auth';
import { UpgradePrompt } from '@/components/billing/upgrade-prompt';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { DifficultyBadge } from '@/components/catalog/difficulty-badge';
import { ConceptVisitTracker } from '@/components/concepts/concept-visit-tracker';
import { ContentNavigation } from '@/components/layout/content-navigation';
import { JsonLdScript } from '@/components/seo/json-ld';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { auth } from '@/lib/auth';
import { userHasPro } from '@/lib/billing/entitlements';
import { getConceptAccess, isContentUnlockedForUser } from '@/lib/billing/tiering';
import { getAllConceptSlugs, getConcept, getAdjacentConcepts } from '@/lib/content/loader';
import { getAdjacentConcepts, getAllConceptSlugs, getConcept } from '@/lib/content/loader';
import { buildPublicMetadata } from '@/lib/seo/build-metadata';
import { learningResourceJsonLd } from '@/lib/seo/structured-data';

Expand Down Expand Up @@ -71,8 +72,9 @@ export default async function ConceptPage({
const adjacent = await getAdjacentConcepts(slug);

return (
<div className="mx-auto max-w-7xl px-6 py-12">
<JsonLdScript
<RequireAuth>
<div className="mx-auto max-w-7xl px-6 py-12">
<JsonLdScript
data={learningResourceJsonLd({
name: concept.meta.title,
description: concept.meta.summary,
Expand Down Expand Up @@ -150,6 +152,7 @@ export default async function ConceptPage({
next={adjacent.next ? { slug: adjacent.next.slug, title: adjacent.next.title, href: `/concepts/${adjacent.next.slug}` } : null}
/>
</div>
</RequireAuth>
);
}

Expand Down
7 changes: 6 additions & 1 deletion app/course/[slug]/module/[moduleId]/certificate/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";

import { RequireAuth } from "@/components/auth/require-auth";
import { ModuleCertificatePageContent } from "@/components/course/module-certificate-page-content";
import {
buildCertificateMetadata,
Expand All @@ -25,5 +26,9 @@ export default async function ModuleCertificatePage({
params: Promise<CertificateParams>;
}) {
const { slug, moduleId } = await params;
return <ModuleCertificatePageContent slug={slug} moduleId={moduleId} />;
return (
<RequireAuth>
<ModuleCertificatePageContent slug={slug} moduleId={moduleId} />
</RequireAuth>
);
}
Loading
Loading