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
3 changes: 3 additions & 0 deletions apps/client/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# URL of the RoastDev server (no trailing slash)
# Copy this file to .env.local for local development
VITE_SERVER_URL=http://localhost:3001
24 changes: 24 additions & 0 deletions apps/client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
34 changes: 34 additions & 0 deletions apps/client/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import prettier from 'eslint-config-prettier';
import { defineConfig } from 'eslint/config';

export default defineConfig([
js.configs.recommended,
prettier,
{
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
ecmaFeatures: { jsx: true },
},
globals: {
...globals.browser,
},
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]);
13 changes: 13 additions & 0 deletions apps/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RoastDev</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src",
"format:check": "prettier --check .",
"test": "vitest run"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"jsdom": "^29.0.2",
"vite": "^8.0.4",
"vitest": "4"
},
"dependencies": {
"canvas-confetti": "^1.9.4",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-icons": "^5.6.0",
"socket.io-client": "^4.8.3"
}
}
1 change: 1 addition & 0 deletions apps/client/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions apps/client/public/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions apps/client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState } from 'react';
import Home from './pages/Home';
import Host from './pages/Host';
import Participant from './pages/Participant';

/**
* App — state-based router.
*
* Three pages, no URL sharing needed, so React Router adds overhead without
* benefit. A `page` string + `sessionData` object is the full routing layer.
*
* sessionData shape: { code: string }
* Set by Home before navigating so Host/Participant receive the session code
* as a prop without any extra fetch.
*/
export default function App() {
const [page, setPage] = useState('home'); // 'home' | 'host' | 'participant'
const [sessionData, setSessionData] = useState(null);

function goHome() {
setSessionData(null);
setPage('home');
}

if (page === 'host') {
return <Host code={sessionData.code} onClose={goHome} />;
}

if (page === 'participant') {
return <Participant code={sessionData.code} onClose={goHome} />;
}

return (
<Home
onJoin={(data) => {
setSessionData(data);
setPage('participant');
}}
onHost={(data) => {
setSessionData(data);
setPage('host');
}}
/>
);
}
8 changes: 8 additions & 0 deletions apps/client/src/components/BadgeLive.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function BadgeLive() {
return (
<div className="badge-live">
<div className="live-dot" />
live
</div>
);
}
59 changes: 59 additions & 0 deletions apps/client/src/components/ConfirmModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect } from 'react';

/**
* Generic confirmation modal.
*
* Props:
* title — heading text
* message — body text explaining the consequence
* confirmLabel — label for the destructive button (default "Confirm")
* onConfirm — called when the user accepts
* onCancel — called when the user dismisses
*
* Accessibility notes:
* • role="dialog" + aria-modal="true" tells screen readers this is a modal
* • aria-labelledby points to the heading so the dialog has a name
* • Escape key closes without confirming — expected keyboard behaviour
* • Clicking the backdrop also cancels
*/
export default function ConfirmModal({
title,
message,
confirmLabel = 'Confirm',
onConfirm,
onCancel,
}) {
// Close on Escape
useEffect(() => {
function onKeyDown(e) {
if (e.key === 'Escape') onCancel();
}
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [onCancel]);

return (
<div className="modal-backdrop" onClick={onCancel} role="presentation">
<div
className="modal-box"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onClick={(e) => e.stopPropagation()}
>
<h2 id="modal-title" className="modal-title">
{title}
</h2>
<p className="modal-message">{message}</p>
<div className="modal-actions">
<button className="btn-ghost btn-sm" onClick={onCancel}>
Cancel
</button>
<button className="btn-danger-outline" onClick={onConfirm}>
{confirmLabel}
</button>
</div>
</div>
</div>
);
}
Loading
Loading