initial commit
This commit is contained in:
117
.cursor/rules/color-theme.mdc
Normal file
117
.cursor/rules/color-theme.mdc
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# Frontend Color Theme Guidelines
|
||||||
|
|
||||||
|
## Color Palette
|
||||||
|
|
||||||
|
### Primary Colors
|
||||||
|
- **Primary Peach**: `#FFCDB2` - Main brand color, used for primary buttons, links, and accents
|
||||||
|
- **Secondary Coral**: `#FFB4A2` - Used for gradients and secondary elements
|
||||||
|
- **Light Pink**: `#E5989B` - Hover states and secondary buttons
|
||||||
|
- **Medium Mauve**: `#B5838D` - Alternative secondary color
|
||||||
|
- **Dark Purple**: `#6D6875` - Text and dark accents
|
||||||
|
|
||||||
|
### Background Colors
|
||||||
|
- **Dark Mode Background**: `#6D6875` - Main dark theme background
|
||||||
|
- **Light Mode Background**: `#FFCDB2` - Main light theme background (very light)
|
||||||
|
- **Card Background**: `white` - Card and component backgrounds
|
||||||
|
|
||||||
|
### Text Colors
|
||||||
|
- **Primary Text (Dark)**: `#6D6875` - Main text color in light mode
|
||||||
|
- **Primary Text (Light)**: `rgba(255, 255, 255, 0.87)` - Main text color in dark mode
|
||||||
|
- **Secondary Text**: `#B5838D` - Subtitle and body text
|
||||||
|
- **Accent Text**: `#E5989B` - Card subtitles and accent text
|
||||||
|
|
||||||
|
### Interactive States
|
||||||
|
- **Hover Effects**: Use `transform: translateY(-2px)` for subtle lift animations
|
||||||
|
- **Focus Rings**: `rgba(255, 205, 178, 0.3)` - Peach focus outline
|
||||||
|
- **Shadows**: `rgba(255, 205, 178, 0.3)` for primary elements, `rgba(229, 152, 155, 0.3)` for secondary
|
||||||
|
|
||||||
|
## CSS Custom Properties
|
||||||
|
|
||||||
|
Always define colors using CSS custom properties in [src/index.css](mdc:src/index.css):
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Primary Colors */
|
||||||
|
--color-primary: #FFCDB2;
|
||||||
|
--color-primary-dark: #FFB4A2;
|
||||||
|
--color-secondary: #E5989B;
|
||||||
|
--color-secondary-dark: #B5838D;
|
||||||
|
--color-dark: #6D6875;
|
||||||
|
|
||||||
|
/* Background Colors */
|
||||||
|
--color-bg-dark: #6D6875;
|
||||||
|
--color-bg-light: #FFCDB2;
|
||||||
|
--color-bg-card: white;
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--color-text-primary: #6D6875;
|
||||||
|
--color-text-primary-light: rgba(255, 255, 255, 0.87);
|
||||||
|
--color-text-secondary: #B5838D;
|
||||||
|
--color-text-accent: #E5989B;
|
||||||
|
|
||||||
|
/* Interactive Colors */
|
||||||
|
--color-focus: rgba(255, 205, 178, 0.3);
|
||||||
|
--color-shadow-primary: rgba(255, 205, 178, 0.3);
|
||||||
|
--color-shadow-secondary: rgba(229, 152, 155, 0.3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Styling Guidelines
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
Follow the patterns in [src/components/Button.css](mdc:src/components/Button.css):
|
||||||
|
- Use gradient backgrounds for primary/secondary buttons
|
||||||
|
- Implement hover animations with `translateY(-2px)`
|
||||||
|
- Apply consistent focus states with green outline
|
||||||
|
- Use semantic class names: `.btn-primary`, `.btn-secondary`, `.btn-outline`, `.btn-ghost`
|
||||||
|
|
||||||
|
### Cards
|
||||||
|
Follow the patterns in [src/components/Card.css](mdc:src/components/Card.css):
|
||||||
|
- Use white background with subtle shadows
|
||||||
|
- Implement hover animations with `translateY(-8px)`
|
||||||
|
- Use consistent border radius (16px)
|
||||||
|
- Apply image hover effects with `scale(1.05)`
|
||||||
|
|
||||||
|
### Links
|
||||||
|
- Primary color: `#FFCDB2`
|
||||||
|
- Hover color: `#E5989B` (dark mode), `#B5838D` (light mode)
|
||||||
|
- No text decoration by default
|
||||||
|
|
||||||
|
## Dark/Light Mode Support
|
||||||
|
|
||||||
|
Always include both dark and light mode variants using `@media (prefers-color-scheme: light)` in [src/index.css](mdc:src/index.css).
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
- Maintain minimum contrast ratios (4.5:1 for normal text, 3:1 for large text)
|
||||||
|
- Use focus indicators with `--color-focus`
|
||||||
|
- Ensure hover states are distinct from focus states
|
||||||
|
- Test with color blindness simulators
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
- Global styles and CSS custom properties: [src/index.css](mdc:src/index.css)
|
||||||
|
- Component-specific styles: Individual `.css` files in [src/components/](mdc:src/components/)
|
||||||
|
- Utility classes: [src/App.css](mdc:src/App.css)
|
||||||
|
- Consider creating a dedicated [src/styles/](mdc:src/styles/) directory for theme files
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Button with theme colors */
|
||||||
|
.my-button {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-dark);
|
||||||
|
box-shadow: 0 4px 12px var(--color-shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card with theme colors */
|
||||||
|
.my-card {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border: 1px solid rgba(109, 104, 117, 0.1);
|
||||||
|
}
|
||||||
|
```
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -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?
|
||||||
222
frontend/README.md
Normal file
222
frontend/README.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# Vibing - React + Vite Application
|
||||||
|
|
||||||
|
A modern React application built with Vite, featuring beautiful components and a responsive design that adapts to any device.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- **Fast Development**: Built with Vite for lightning-fast development and hot module replacement
|
||||||
|
- **Modern React**: Using React 19 with modern hooks and patterns
|
||||||
|
- **Responsive Design**: Fully responsive design that looks great on all devices
|
||||||
|
- **Component Library**: Reusable components with consistent styling
|
||||||
|
- **Custom Hooks**: Useful custom hooks for common functionality
|
||||||
|
- **API Utilities**: Built-in API utilities with error handling
|
||||||
|
- **Modern Styling**: Beautiful gradients and modern CSS design
|
||||||
|
|
||||||
|
## 📦 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # Reusable UI components
|
||||||
|
│ ├── Header.jsx # Navigation header
|
||||||
|
│ ├── Footer.jsx # Site footer
|
||||||
|
│ ├── Button.jsx # Reusable button component
|
||||||
|
│ ├── Card.jsx # Card component for content
|
||||||
|
│ └── index.js # Component exports
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
│ └── useLocalStorage.js
|
||||||
|
├── pages/ # Page components
|
||||||
|
│ ├── Home.jsx # Home page
|
||||||
|
│ └── Home.css
|
||||||
|
├── utils/ # Utility functions
|
||||||
|
│ └── api.js # API utilities
|
||||||
|
├── styles/ # Global styles (future use)
|
||||||
|
├── assets/ # Static assets
|
||||||
|
├── App.jsx # Main app component
|
||||||
|
├── App.css # Global styles
|
||||||
|
├── main.jsx # App entry point
|
||||||
|
└── index.css # Base styles
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (version 16 or higher)
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd vibing
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Open your browser and navigate to `http://localhost:5173`
|
||||||
|
|
||||||
|
## 📜 Available Scripts
|
||||||
|
|
||||||
|
- `npm run dev` - Start development server
|
||||||
|
- `npm run build` - Build for production
|
||||||
|
- `npm run preview` - Preview production build
|
||||||
|
- `npm run lint` - Run ESLint
|
||||||
|
|
||||||
|
## 🎨 Components
|
||||||
|
|
||||||
|
### Button Component
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Button } from './components';
|
||||||
|
|
||||||
|
// Different variants
|
||||||
|
<Button variant="primary">Primary Button</Button>
|
||||||
|
<Button variant="secondary">Secondary Button</Button>
|
||||||
|
<Button variant="outline">Outline Button</Button>
|
||||||
|
<Button variant="ghost">Ghost Button</Button>
|
||||||
|
|
||||||
|
// Different sizes
|
||||||
|
<Button size="small">Small</Button>
|
||||||
|
<Button size="medium">Medium</Button>
|
||||||
|
<Button size="large">Large</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Card Component
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Card } from './components';
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="Card Title"
|
||||||
|
subtitle="Card subtitle"
|
||||||
|
image="https://example.com/image.jpg"
|
||||||
|
>
|
||||||
|
<p>Card content goes here...</p>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Header & Footer
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Header, Footer } from './components';
|
||||||
|
|
||||||
|
// Header with navigation
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
// Footer with links and social media
|
||||||
|
<Footer />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Custom Hooks
|
||||||
|
|
||||||
|
### useLocalStorage
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useLocalStorage from './hooks/useLocalStorage';
|
||||||
|
|
||||||
|
const [value, setValue] = useLocalStorage('key', initialValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 API Utilities
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { api } from './utils/api';
|
||||||
|
|
||||||
|
// GET request
|
||||||
|
const data = await api.get('/users');
|
||||||
|
|
||||||
|
// POST request
|
||||||
|
const newUser = await api.post('/users', { name: 'John', email: 'john@example.com' });
|
||||||
|
|
||||||
|
// PUT request
|
||||||
|
const updatedUser = await api.put('/users/1', { name: 'Jane' });
|
||||||
|
|
||||||
|
// DELETE request
|
||||||
|
await api.delete('/users/1');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file in the root directory:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_BASE_URL=http://localhost:3000/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Responsive Design
|
||||||
|
|
||||||
|
The application is fully responsive and includes:
|
||||||
|
|
||||||
|
- Mobile-first design approach
|
||||||
|
- Flexible grid layouts
|
||||||
|
- Responsive typography
|
||||||
|
- Touch-friendly interactions
|
||||||
|
- Optimized for all screen sizes
|
||||||
|
|
||||||
|
## 🎨 Styling
|
||||||
|
|
||||||
|
The project uses:
|
||||||
|
|
||||||
|
- CSS modules for component-specific styles
|
||||||
|
- CSS custom properties for theming
|
||||||
|
- Flexbox and Grid for layouts
|
||||||
|
- Modern CSS features like gradients and animations
|
||||||
|
- Responsive design patterns
|
||||||
|
|
||||||
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
### Build for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The build output will be in the `dist` directory, ready for deployment to any static hosting service.
|
||||||
|
|
||||||
|
### Deploy to Vercel
|
||||||
|
|
||||||
|
1. Install Vercel CLI:
|
||||||
|
```bash
|
||||||
|
npm i -g vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Deploy:
|
||||||
|
```bash
|
||||||
|
vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to Netlify
|
||||||
|
|
||||||
|
1. Build the project:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Upload the `dist` folder to Netlify
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- [Vite](https://vitejs.dev/) for the fast build tool
|
||||||
|
- [React](https://reactjs.org/) for the UI library
|
||||||
|
- [Unsplash](https://unsplash.com/) for the beautiful images
|
||||||
10
frontend/env.example
Normal file
10
frontend/env.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# API Configuration
|
||||||
|
VITE_API_BASE_URL=http://localhost:3000/api
|
||||||
|
|
||||||
|
# App Configuration
|
||||||
|
VITE_APP_TITLE=Vibing
|
||||||
|
VITE_APP_DESCRIPTION=A modern React application built with Vite
|
||||||
|
|
||||||
|
# Feature Flags
|
||||||
|
VITE_ENABLE_ANALYTICS=false
|
||||||
|
VITE_ENABLE_DEBUG_MODE=false
|
||||||
29
frontend/eslint.config.js
Normal file
29
frontend/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2850
frontend/package-lock.json
generated
Normal file
2850
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
frontend/package.json
Normal file
28
frontend/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "vibing",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.30.1",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"eslint": "^9.30.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"vite": "^7.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
75
frontend/src/App.css
Normal file
75
frontend/src/App.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* Global styles */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset some default styles */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive utilities */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
frontend/src/App.jsx
Normal file
19
frontend/src/App.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { LanguageProvider } from './contexts/LanguageContext';
|
||||||
|
import Home from './pages/Home';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<LanguageProvider>
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</LanguageProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
87
frontend/src/components/Button.css
Normal file
87
frontend/src/components/Button.css
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px var(--color-shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px var(--color-shadow-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-primary);
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover:not(:disabled) {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 205, 178, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-medium {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus states */
|
||||||
|
.btn:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--color-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:focus {
|
||||||
|
box-shadow: 0 0 0 3px var(--color-focus);
|
||||||
|
}
|
||||||
29
frontend/src/components/Button.jsx
Normal file
29
frontend/src/components/Button.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Button.css';
|
||||||
|
|
||||||
|
const Button = ({
|
||||||
|
children,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'medium',
|
||||||
|
onClick,
|
||||||
|
disabled = false,
|
||||||
|
type = 'button',
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const buttonClasses = `btn btn-${variant} btn-${size} ${className}`.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
className={buttonClasses}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
69
frontend/src/components/Card.css
Normal file
69
frontend/src/components/Card.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
.card {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid rgba(109, 104, 117, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .card-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card without image */
|
||||||
|
.card:not(:has(.card-image)) .card-content {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:not(:has(.card-image)) .card-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
frontend/src/components/Card.jsx
Normal file
31
frontend/src/components/Card.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Card.css';
|
||||||
|
|
||||||
|
const Card = ({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
image,
|
||||||
|
imageAlt,
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`card ${className}`.trim()} {...props}>
|
||||||
|
{image && (
|
||||||
|
<div className="card-image">
|
||||||
|
<img src={image} alt={imageAlt || title} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="card-content">
|
||||||
|
{title && <h3 className="card-title">{title}</h3>}
|
||||||
|
{subtitle && <p className="card-subtitle">{subtitle}</p>}
|
||||||
|
<div className="card-body">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
||||||
76
frontend/src/components/Footer.css
Normal file
76
frontend/src/components/Footer.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
.footer {
|
||||||
|
background: linear-gradient(135deg, var(--color-dark) 0%, var(--color-secondary-dark) 100%);
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
padding: 3rem 0 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h4 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul li a {
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul li a:hover {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid var(--color-secondary-dark);
|
||||||
|
padding-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/src/components/Footer.jsx
Normal file
40
frontend/src/components/Footer.jsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Footer.css';
|
||||||
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
return (
|
||||||
|
<footer className="footer">
|
||||||
|
<div className="footer-container">
|
||||||
|
<div className="footer-content">
|
||||||
|
<div className="footer-section">
|
||||||
|
<h3>Phun</h3>
|
||||||
|
<p>{t('footerDescription')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="footer-section">
|
||||||
|
<h4>Links</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/about">About</a></li>
|
||||||
|
<li><a href="/contact">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="footer-section">
|
||||||
|
<h4>Connect</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com" target="_blank" rel="noopener noreferrer">GitHub</a></li>
|
||||||
|
<li><a href="https://twitter.com" target="_blank" rel="noopener noreferrer">Twitter</a></li>
|
||||||
|
<li><a href="https://linkedin.com" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="footer-bottom">
|
||||||
|
<p>© 2024 Phun. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
68
frontend/src/components/Header.css
Normal file
68
frontend/src/components/Header.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||||
|
color: var(--color-dark);
|
||||||
|
padding: 1rem 0;
|
||||||
|
box-shadow: 0 2px 10px var(--color-shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(45deg, var(--color-dark), var(--color-secondary-dark));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--color-dark);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
frontend/src/components/Header.jsx
Normal file
27
frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import LanguageSwitcher from './LanguageSwitcher';
|
||||||
|
import './Header.css';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="header">
|
||||||
|
<div className="header-container">
|
||||||
|
<h1 className="header-title">Phun</h1>
|
||||||
|
<nav className="header-nav">
|
||||||
|
<ul className="nav-list">
|
||||||
|
<li><Link to="/" className="nav-link">{t('home')}</Link></li>
|
||||||
|
<li><Link to="/about" className="nav-link">{t('about')}</Link></li>
|
||||||
|
<li><Link to="/contact" className="nav-link">{t('contact')}</Link></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
89
frontend/src/components/LanguageSwitcher.css
Normal file
89
frontend/src/components/LanguageSwitcher.css
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
.language-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(109, 104, 117, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn:hover {
|
||||||
|
background: rgba(255, 205, 178, 0.2);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-dark);
|
||||||
|
box-shadow: 0 2px 8px var(--color-shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn:focus {
|
||||||
|
outline: 2px solid var(--color-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode adjustments */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.language-buttons {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn {
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn:hover {
|
||||||
|
background: rgba(255, 205, 178, 0.15);
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.language-switcher {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-label {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
frontend/src/components/LanguageSwitcher.jsx
Normal file
35
frontend/src/components/LanguageSwitcher.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import './LanguageSwitcher.css';
|
||||||
|
|
||||||
|
const LanguageSwitcher = () => {
|
||||||
|
const { language, setLanguage, t } = useLanguage();
|
||||||
|
|
||||||
|
const handleLanguageChange = (newLanguage) => {
|
||||||
|
setLanguage(newLanguage);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="language-switcher">
|
||||||
|
<span className="language-label">{t('language')}:</span>
|
||||||
|
<div className="language-buttons">
|
||||||
|
<button
|
||||||
|
className={`language-btn ${language === 'en' ? 'active' : ''}`}
|
||||||
|
onClick={() => handleLanguageChange('en')}
|
||||||
|
aria-label="Switch to English"
|
||||||
|
>
|
||||||
|
{t('english')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`language-btn ${language === 'fi' ? 'active' : ''}`}
|
||||||
|
onClick={() => handleLanguageChange('fi')}
|
||||||
|
aria-label="Vaihda suomeksi"
|
||||||
|
>
|
||||||
|
{t('finnish')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageSwitcher;
|
||||||
5
frontend/src/components/index.js
Normal file
5
frontend/src/components/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as Header } from './Header';
|
||||||
|
export { default as Footer } from './Footer';
|
||||||
|
export { default as Button } from './Button';
|
||||||
|
export { default as Card } from './Card';
|
||||||
|
export { default as LanguageSwitcher } from './LanguageSwitcher';
|
||||||
103
frontend/src/contexts/LanguageContext.jsx
Normal file
103
frontend/src/contexts/LanguageContext.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
const LanguageContext = createContext();
|
||||||
|
|
||||||
|
export const useLanguage = () => {
|
||||||
|
const context = useContext(LanguageContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
// Header
|
||||||
|
home: 'Home',
|
||||||
|
about: 'About',
|
||||||
|
contact: 'Contact',
|
||||||
|
|
||||||
|
// CTA Section
|
||||||
|
readyToStart: 'Ready to get started?',
|
||||||
|
ctaSubtitle: 'Join thousands of developers building amazing applications with our modern stack.',
|
||||||
|
startBuilding: 'Start Building',
|
||||||
|
|
||||||
|
// Language Switcher
|
||||||
|
language: 'Language',
|
||||||
|
english: 'English',
|
||||||
|
finnish: 'Finnish',
|
||||||
|
|
||||||
|
// Search Page
|
||||||
|
searchPlaceholder: 'Enter your search query...',
|
||||||
|
searchButton: 'Search',
|
||||||
|
searchResults: 'Search Results',
|
||||||
|
searchResultsSubtitle: 'Here are the results for your search query.',
|
||||||
|
searchResultsPlaceholder: 'Search results will appear here. Try entering a search term above to get started.',
|
||||||
|
|
||||||
|
// Search Filters
|
||||||
|
dogFriendly: 'Dog Friendly',
|
||||||
|
accessible: 'Accessible',
|
||||||
|
familyFriendly: 'Family Friendly',
|
||||||
|
peopleCount: 'People',
|
||||||
|
priceRange: 'Price',
|
||||||
|
any: 'Any',
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
footerDescription: 'Find fun things to do!',
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
// Header
|
||||||
|
home: 'Koti',
|
||||||
|
about: 'Tietoja',
|
||||||
|
contact: 'Yhteystiedot',
|
||||||
|
|
||||||
|
|
||||||
|
// CTA Section
|
||||||
|
readyToStart: 'Valmiina aloittamaan?',
|
||||||
|
ctaSubtitle: 'Liity tuhansien kehittäjien joukkoon, jotka rakentavat upeita sovelluksia modernilla teknologiapinoamme.',
|
||||||
|
startBuilding: 'Aloita rakentaminen',
|
||||||
|
|
||||||
|
// Language Switcher
|
||||||
|
language: 'Kieli',
|
||||||
|
english: 'Englanti',
|
||||||
|
finnish: 'Suomi',
|
||||||
|
|
||||||
|
// Search Page
|
||||||
|
searchPlaceholder: 'Syötä hakukyselysi...',
|
||||||
|
searchButton: 'Hae',
|
||||||
|
searchResults: 'Hakutulokset',
|
||||||
|
searchResultsSubtitle: 'Tässä ovat hakutulokset kyselysi perusteella.',
|
||||||
|
searchResultsPlaceholder: 'Hakutulokset näkyvät täällä. Kokeile syöttää hakusana yllä aloittaaksesi.',
|
||||||
|
|
||||||
|
// Search Filters
|
||||||
|
dogFriendly: 'Koiraystävällinen',
|
||||||
|
accessible: 'Esteetön',
|
||||||
|
familyFriendly: 'Lapsiystävällinen',
|
||||||
|
peopleCount: 'Henkilöt',
|
||||||
|
priceRange: 'Hinta',
|
||||||
|
any: 'Mikä tahansa',
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
footerDescription: 'Löydä hauskaa tekemistä!',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LanguageProvider = ({ children }) => {
|
||||||
|
const [language, setLanguage] = useState('en');
|
||||||
|
|
||||||
|
const t = (key) => {
|
||||||
|
return translations[language][key] || key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
language,
|
||||||
|
setLanguage,
|
||||||
|
t
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LanguageContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</LanguageContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
frontend/src/hooks/useLocalStorage.js
Normal file
30
frontend/src/hooks/useLocalStorage.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const useLocalStorage = (key, initialValue) => {
|
||||||
|
// Get from local storage then parse stored json or return initialValue
|
||||||
|
const [storedValue, setStoredValue] = useState(() => {
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
return item ? JSON.parse(item) : initialValue;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading localStorage key "${key}":`, error);
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a wrapped version of useState's setter function that persists the new value to localStorage
|
||||||
|
const setValue = (value) => {
|
||||||
|
try {
|
||||||
|
// Allow value to be a function so we have the same API as useState
|
||||||
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||||
|
setStoredValue(valueToStore);
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error setting localStorage key "${key}":`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [storedValue, setValue];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLocalStorage;
|
||||||
89
frontend/src/index.css
Normal file
89
frontend/src/index.css
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #6D6875;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
/* Primary Colors */
|
||||||
|
--color-primary: #FFCDB2;
|
||||||
|
--color-primary-dark: #FFB4A2;
|
||||||
|
--color-secondary: #E5989B;
|
||||||
|
--color-secondary-dark: #B5838D;
|
||||||
|
--color-dark: #6D6875;
|
||||||
|
|
||||||
|
/* Background Colors */
|
||||||
|
--color-bg-dark: #6D6875;
|
||||||
|
--color-bg-light: #FFCDB2;
|
||||||
|
--color-bg-card: white;
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--color-text-primary: #6D6875;
|
||||||
|
--color-text-primary-light: rgba(255, 255, 255, 0.87);
|
||||||
|
--color-text-secondary: #B5838D;
|
||||||
|
--color-text-accent: #E5989B;
|
||||||
|
|
||||||
|
/* Interactive Colors */
|
||||||
|
--color-focus: rgba(255, 205, 178, 0.3);
|
||||||
|
--color-shadow-primary: rgba(255, 205, 178, 0.3);
|
||||||
|
--color-shadow-secondary: rgba(229, 152, 155, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: var(--color-bg-dark);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto var(--color-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-secondary-dark);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
337
frontend/src/pages/Home.css
Normal file
337
frontend/src/pages/Home.css
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
.home {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero {
|
||||||
|
background: linear-gradient(rgba(109, 104, 117, 0.4), rgba(109, 104, 117, 0.4)), url('https://images.unsplash.com/photo-1429305336325-b84ace7eba3b?q=80&w=1770&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: white;
|
||||||
|
padding: 3rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0 0 3rem 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
background: linear-gradient(45deg, var(--color-primary), var(--color-secondary));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Toolbar in Hero */
|
||||||
|
.search-toolbar {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Input Container */
|
||||||
|
.search-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-shadow: 0 8px 32px var(--color-shadow-primary);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 40px var(--color-shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 25px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px var(--color-shadow-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px var(--color-shadow-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:focus {
|
||||||
|
outline: 2px solid var(--color-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters Container */
|
||||||
|
.filters-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 25px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(109, 104, 117, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(109, 104, 117, 0.15);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--color-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:hover {
|
||||||
|
background: rgba(229, 152, 155, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:focus {
|
||||||
|
background: rgba(229, 152, 155, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Features Section */
|
||||||
|
.features {
|
||||||
|
padding: 5rem 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 3rem 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-content {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container {
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input,
|
||||||
|
.search-button {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-container {
|
||||||
|
gap: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
min-width: 200px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-toolbar {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
min-width: 180px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.search-toolbar {
|
||||||
|
background: rgba(109, 104, 117, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-text {
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
background: var(--color-bg-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
150
frontend/src/pages/Home.jsx
Normal file
150
frontend/src/pages/Home.jsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Header, Footer, Button, Card } from '../components';
|
||||||
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import './Home.css';
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
dogFriendly: false,
|
||||||
|
accessible: false,
|
||||||
|
familyFriendly: false,
|
||||||
|
peopleCount: '',
|
||||||
|
priceRange: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleGetStarted = () => {
|
||||||
|
alert('Get Started button clicked!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLearnMore = () => {
|
||||||
|
alert('Learn More button clicked!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (filterName, value) => {
|
||||||
|
setFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[filterName]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
// Handle search logic here
|
||||||
|
console.log('Search query:', searchQuery);
|
||||||
|
console.log('Filters:', filters);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="home">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main className="main-content">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="hero">
|
||||||
|
<div className="hero-content">
|
||||||
|
|
||||||
|
{/* Search Toolbar */}
|
||||||
|
<div className="search-toolbar">
|
||||||
|
<div className="toolbar-content">
|
||||||
|
{/* Search Input */}
|
||||||
|
<div className="search-input-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="search-input"
|
||||||
|
placeholder={t('searchPlaceholder')}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
/>
|
||||||
|
<button className="search-button" onClick={handleSearch}>
|
||||||
|
{t('searchButton')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="filters-container">
|
||||||
|
<div className="filter-group">
|
||||||
|
<label className="filter-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="filter-checkbox"
|
||||||
|
checked={filters.dogFriendly}
|
||||||
|
onChange={(e) => handleFilterChange('dogFriendly', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="filter-text">{t('dogFriendly')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filter-group">
|
||||||
|
<label className="filter-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="filter-checkbox"
|
||||||
|
checked={filters.accessible}
|
||||||
|
onChange={(e) => handleFilterChange('accessible', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="filter-text">{t('accessible')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filter-group">
|
||||||
|
<label className="filter-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="filter-checkbox"
|
||||||
|
checked={filters.familyFriendly}
|
||||||
|
onChange={(e) => handleFilterChange('familyFriendly', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="filter-text">{t('familyFriendly')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filter-group">
|
||||||
|
<label className="filter-label">
|
||||||
|
<span className="filter-text">{t('peopleCount')}:</span>
|
||||||
|
<select
|
||||||
|
className="filter-select"
|
||||||
|
value={filters.peopleCount}
|
||||||
|
onChange={(e) => handleFilterChange('peopleCount', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">{t('any')}</option>
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
<option value="5+">5+</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filter-group">
|
||||||
|
<label className="filter-label">
|
||||||
|
<span className="filter-text">{t('priceRange')}:</span>
|
||||||
|
<select
|
||||||
|
className="filter-select"
|
||||||
|
value={filters.priceRange}
|
||||||
|
onChange={(e) => handleFilterChange('priceRange', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">{t('any')}</option>
|
||||||
|
<option value="€">€</option>
|
||||||
|
<option value="€€">€€</option>
|
||||||
|
<option value="€€€">€€€</option>
|
||||||
|
<option value="€€€€">€€€€</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
84
frontend/src/utils/api.js
Normal file
84
frontend/src/utils/api.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';
|
||||||
|
|
||||||
|
class ApiError extends Error {
|
||||||
|
constructor(message, status, data) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.status = status;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResponse = async (response) => {
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
const isJson = contentType && contentType.includes('application/json');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = isJson ? await response.json() : await response.text();
|
||||||
|
throw new ApiError(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`,
|
||||||
|
response.status,
|
||||||
|
errorData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isJson ? response.json() : response.text();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
async get(endpoint, options = {}) {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return handleResponse(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async post(endpoint, data = {}, options = {}) {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return handleResponse(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async put(endpoint, data = {}, options = {}) {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return handleResponse(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(endpoint, options = {}) {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return handleResponse(response);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ApiError };
|
||||||
7
frontend/vite.config.js
Normal file
7
frontend/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user