feat: scaffold frontend (Vite + React + Vitest)

- Añade scaffold de frontend con Vite y React
- Configura Vitest y tests básicos (App, Navbar)
- Añade QueryClient y hooks/plantillas iniciales
This commit is contained in:
2026-02-09 20:22:24 +01:00
parent 79c42fad55
commit 08aca0fd5b
27 changed files with 3171 additions and 215 deletions

36
frontend/index.html Normal file
View File

@@ -0,0 +1,36 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quasar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quasar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quasar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

34
frontend/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "quasar-frontend",
"version": "0.0.0",
"private": true,
"packageManager": "yarn@4.12.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run",
"lint": "echo \"No lint configured\"",
"format": "prettier --write ."
},
"dependencies": {
"@tanstack/react-query": "^4.34.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"jsdom": "^22.1.0",
"postcss": "^8.4.24",
"tailwindcss": "^3.4.7",
"typescript": "^5.2.2",
"vite": "^5.1.0",
"vitest": "^0.34.1"
}
}

View File

@@ -0,0 +1,18 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

13
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import Navbar from './components/layout/Navbar';
export default function App(): JSX.Element {
return (
<div>
<Navbar />
<main>
<h1>Quasar</h1>
</main>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import React from 'react';
export default function Navbar(): JSX.Element {
return (
<nav style={{ padding: 12 }}>
<a href="/roms" style={{ marginRight: 12 }}>
ROMs
</a>
<a href="/games">Games</a>
</nav>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function Sidebar(): JSX.Element {
return (
<aside style={{ padding: 12 }}>
<div>Sidebar (placeholder)</div>
</aside>
);
}

View File

@@ -0,0 +1,4 @@
export function useGames() {
// placeholder stub for tests and future implementation
return { data: [], isLoading: false };
}

3
frontend/src/lib/api.ts Normal file
View File

@@ -0,0 +1,3 @@
export const api = {
// placeholder for future HTTP client
};

View File

@@ -0,0 +1,3 @@
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient();

32
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,32 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './lib/queryClient';
import App from './App';
import './styles.css';
const rootEl = document.getElementById('root');
if (rootEl) {
createRoot(rootEl).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './lib/queryClient';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function Games(): JSX.Element {
return (
<div>
<h2>Games</h2>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function Home(): JSX.Element {
return (
<div>
<h2>Home</h2>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function Roms(): JSX.Element {
return (
<div>
<h2>ROMs</h2>
</div>
);
}

View File

@@ -0,0 +1,2 @@
import '@testing-library/jest-dom';
import '@testing-library/jest-dom';

8
frontend/src/styles.css Normal file
View File

@@ -0,0 +1,8 @@
/* Minimal global styles */
html, body, #root {
height: 100%;
}
body {
margin: 0;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
}

View File

@@ -0,0 +1,21 @@
module.exports = {
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
module.exports = {
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
module.exports = {
content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from '../src/App';
describe('App', () => {
it('renderiza el título Quasar', () => {
render(<App />);
expect(screen.getByText('Quasar')).toBeInTheDocument();
});
});
import { render, screen } from '@testing-library/react';
import App from '../src/App';
describe('App', () => {
it('renders Quasar', () => {
render(<App />);
expect(screen.getByText(/Quasar/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import Navbar from '../../src/components/layout/Navbar';
describe('Navbar', () => {
it('muestra enlaces ROMs y Games', () => {
render(<Navbar />);
expect(screen.getByText('ROMs')).toBeInTheDocument();
expect(screen.getByText('Games')).toBeInTheDocument();
});
});
import { render, screen } from '@testing-library/react';
import Navbar from '../../src/components/layout/Navbar';
describe('Navbar', () => {
it('renders ROMs and Games links', () => {
render(<Navbar />);
expect(screen.getByText(/ROMs/)).toBeInTheDocument();
expect(screen.getByText(/Games/)).toBeInTheDocument();
});
});

19
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"types": ["vite/client", "vitest/globals"]
},
"include": ["src", "tests"]
}

28
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,28 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
},
});
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/setupTests.ts'],
include: ['tests/**/*.spec.tsx'],
},
});
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 5173 },
});

10
frontend/vitest.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.ts',
include: ['tests/**/*.spec.tsx'],
},
});