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
49 changes: 49 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Deploy to GitHub Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Build for GitHub Pages
run: pnpm build
env:
GITHUB_PAGES: 'true'

- uses: actions/upload-pages-artifact@v3
with:
path: apps/web/dist

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v4
id: deployment
18 changes: 15 additions & 3 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@ on:
- 'packages/**'

jobs:
build-linux:
runs-on: ubuntu-22.04
build:
strategy:
fail-fast: false
matrix:
include:
- platform: linux
os: ubuntu-22.04
- platform: macos
os: macos-latest
- platform: windows
os: windows-latest

runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

Expand All @@ -24,7 +35,8 @@ jobs:
node-version: 22
cache: pnpm

- name: Install system dependencies
- name: Install Linux dependencies
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/lighthouse.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Lighthouse

on:
pull_request:
branches: [main]
paths:
- 'apps/web/**'
- 'packages/**'
- 'lighthouserc.json'
push:
branches: [main]
paths:
- 'apps/web/**'
- 'packages/**'
- 'lighthouserc.json'

jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm build

- name: Serve production build
run: |
pnpm --filter @coderkeys/web exec serve dist -l 4173 &
sleep 2
curl -sf http://localhost:4173 > /dev/null

- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
configPath: ./lighthouserc.json
uploadArtifacts: true
urls: |
http://localhost:4173
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ coverage
test-results
playwright-report
src-tauri/target
.lighthouseci
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CoderKeys

[![CI](https://github.com/iagommendes/coderkeys/actions/workflows/ci.yml/badge.svg)](https://github.com/iagommendes/coderkeys/actions/workflows/ci.yml)
[![Deploy](https://github.com/iagommendes/coderkeys/actions/workflows/deploy.yml/badge.svg)](https://github.com/iagommendes/coderkeys/actions/workflows/deploy.yml)
[![Live Demo](https://img.shields.io/badge/demo-GitHub%20Pages-blue)](https://iagommendes.github.io/coderkeys/)

Open-source, local-first typing tutor tailored for **programmers** and **technical writers** to master code syntax and real-world vocabularies.

## Features
Expand All @@ -25,6 +29,8 @@ pnpm dev

Open [http://localhost:5173](http://localhost:5173) in your browser.

**Live demo:** [https://iagommendes.github.io/coderkeys/](https://iagommendes.github.io/coderkeys/)

### Other commands

```bash
Expand Down
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build": "tsc -b && vite build && node scripts/copy-404.mjs",
"preview": "vite preview",
"typecheck": "tsc -b --noEmit",
"test": "vitest run --passWithNoTests",
Expand All @@ -31,6 +31,7 @@
"devDependencies": {
"@codemirror/lang-javascript": "^6.2.3",
"@playwright/test": "^1.51.1",
"serve": "^14.2.4",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.36.4",
Expand Down
11 changes: 11 additions & 0 deletions apps/web/scripts/copy-404.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { copyFileSync, existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const dist = path.join(path.dirname(fileURLToPath(import.meta.url)), '../dist');
const index = path.join(dist, 'index.html');

if (existsSync(index)) {
copyFileSync(index, path.join(dist, '404.html'));
console.log('✓ Copied index.html → 404.html for SPA routing on GitHub Pages');
}
73 changes: 63 additions & 10 deletions apps/web/src/app/router.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,79 @@
import { lazy, Suspense } from 'react';
import { BrowserRouter, Navigate, Route, Routes, useParams } from 'react-router-dom';
import { Layout } from '@/shared/components/Layout';
import { CatalogPage, TrackPage } from '@/features/catalog/pages/CatalogPage';
import { DashboardPage } from '@/features/dashboard/pages/DashboardPage';
import { LessonPage } from '@/features/lesson/pages/LessonPage';
import { SettingsPage } from '@/features/settings/pages/SettingsPage';
import { PageLoader } from '@/shared/components/PageLoader';

const CatalogPage = lazy(() =>
import('@/features/catalog/pages/CatalogPage').then((m) => ({ default: m.CatalogPage })),
);
const TrackPage = lazy(() =>
import('@/features/catalog/pages/CatalogPage').then((m) => ({ default: m.TrackPage })),
);
const DashboardPage = lazy(() =>
import('@/features/dashboard/pages/DashboardPage').then((m) => ({ default: m.DashboardPage })),
);
const LessonPage = lazy(() =>
import('@/features/lesson/pages/LessonPage').then((m) => ({ default: m.LessonPage })),
);
const SettingsPage = lazy(() =>
import('@/features/settings/pages/SettingsPage').then((m) => ({ default: m.SettingsPage })),
);

function TrackRoute() {
const { trackId } = useParams<{ trackId: string }>();
if (!trackId) return <Navigate to="/" replace />;
return <TrackPage trackId={trackId} />;
}

function LazyPage({ children }: { children: React.ReactNode }) {
return <Suspense fallback={<PageLoader />}>{children}</Suspense>;
}

export function AppRouter() {
return (
<BrowserRouter>
<BrowserRouter basename={import.meta.env.BASE_URL}>
<Layout>
<Routes>
<Route path="/" element={<CatalogPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/tracks/:trackId" element={<TrackRoute />} />
<Route path="/lessons/:lessonId" element={<LessonPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route
path="/"
element={
<LazyPage>
<CatalogPage />
</LazyPage>
}
/>
<Route
path="/dashboard"
element={
<LazyPage>
<DashboardPage />
</LazyPage>
}
/>
<Route
path="/tracks/:trackId"
element={
<LazyPage>
<TrackRoute />
</LazyPage>
}
/>
<Route
path="/lessons/:lessonId"
element={
<LazyPage>
<LessonPage />
</LazyPage>
}
/>
<Route
path="/settings"
element={
<LazyPage>
<SettingsPage />
</LazyPage>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
Expand Down
15 changes: 12 additions & 3 deletions apps/web/src/shared/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,32 @@ export function Header() {
<NavLink
to="/"
className={({ isActive }) =>
cn('text-muted transition hover:text-foreground', isActive && 'text-accent')
cn(
'text-muted transition hover:text-foreground',
isActive && 'font-semibold text-foreground',
)
}
>
{t('nav.catalog')}
</NavLink>
<NavLink
to="/dashboard"
className={({ isActive }) =>
cn('text-muted transition hover:text-foreground', isActive && 'text-accent')
cn(
'text-muted transition hover:text-foreground',
isActive && 'font-semibold text-foreground',
)
}
>
{t('nav.dashboard')}
</NavLink>
<NavLink
to="/settings"
className={({ isActive }) =>
cn('text-muted transition hover:text-foreground', isActive && 'text-accent')
cn(
'text-muted transition hover:text-foreground',
isActive && 'font-semibold text-foreground',
)
}
>
{t('nav.settings')}
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/shared/components/PageLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function PageLoader() {
return (
<div className="flex min-h-[40vh] items-center justify-center text-muted" role="status">
Loading...
</div>
);
}
4 changes: 2 additions & 2 deletions apps/web/src/themes/default-dark.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"surface": "#0f1419",
"surfaceElevated": "#1a2332",
"border": "#2d3a4f",
"accent": "#3b82f6",
"accentHover": "#2563eb",
"accent": "#60a5fa",
"accentHover": "#3b82f6",
"success": "#22c55e",
"error": "#ef4444",
"muted": "#94a3b8",
Expand Down
Loading
Loading