react_minta
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# React frontend minta
|
||||||
|
|
||||||
|
Ez a mini projekt a Frontend PPT React temai alapjan keszult, es bemutatja:
|
||||||
|
|
||||||
|
- komponens alapu felepitest
|
||||||
|
- allapotkezelest useState hookkal
|
||||||
|
- adatbetoltest useEffect + Axios segitsegevel
|
||||||
|
- kliensoldali routingot React Routerrel
|
||||||
|
- Context API hasznalatat tema valtassal
|
||||||
|
- kontrollalt urlapot valos ideju validacioval
|
||||||
|
|
||||||
|
## Inditas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build ellenorzes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fo oldalak
|
||||||
|
|
||||||
|
- `/` fooldal, useState szamlalo
|
||||||
|
- `/termekek` lista API-bol, szures, loading/error/allapotok
|
||||||
|
- `/termekek/:id` dinamikus route reszletoldal
|
||||||
|
- `/kapcsolat` kontrollalt form validacioval
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="hu">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>React Frontend Minta</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "react-minta-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.30.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import Layout from "./components/Layout";
|
||||||
|
import HomePage from "./pages/HomePage";
|
||||||
|
import ProductsPage from "./pages/ProductsPage";
|
||||||
|
import ProductDetailsPage from "./pages/ProductDetailsPage";
|
||||||
|
import ContactPage from "./pages/ContactPage";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/termekek" element={<ProductsPage />} />
|
||||||
|
<Route path="/termekek/:id" element={<ProductDetailsPage />} />
|
||||||
|
<Route path="/kapcsolat" element={<ContactPage />} />
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: "https://fakestoreapi.com",
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function fetchProducts() {
|
||||||
|
const response = await api.get("/products");
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProductById(id) {
|
||||||
|
const response = await api.get(`/products/${id}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { useTheme } from "../context/ThemeContext";
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ to: "/", label: "Fooldal" },
|
||||||
|
{ to: "/termekek", label: "Termekek" },
|
||||||
|
{ to: "/kapcsolat", label: "Kapcsolat" }
|
||||||
|
];
|
||||||
|
|
||||||
|
function Layout({ children }) {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`app-shell ${theme}`}>
|
||||||
|
<header className="topbar">
|
||||||
|
<h1>React Frontend Minta</h1>
|
||||||
|
<nav>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.to}
|
||||||
|
to={item.to}
|
||||||
|
className={({ isActive }) => `nav-link${isActive ? " active" : ""}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<button className="theme-button" onClick={toggleTheme}>
|
||||||
|
Tema: {theme === "light" ? "Vilagos" : "Sotet"}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<main className="page-content">{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
function ProductCard({ product, onAddToCart }) {
|
||||||
|
return (
|
||||||
|
<article className="product-card">
|
||||||
|
<img src={product.image} alt={product.title} loading="lazy" />
|
||||||
|
<h3>{product.title}</h3>
|
||||||
|
<p className="category">{product.category}</p>
|
||||||
|
<p className="price">{product.price.toFixed(2)} USD</p>
|
||||||
|
<div className="card-actions">
|
||||||
|
<button onClick={() => onAddToCart(product)}>Kosarba</button>
|
||||||
|
<Link to={`/termekek/${product.id}`}>Reszletek</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCard;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createContext, useContext, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const ThemeContext = createContext({
|
||||||
|
theme: "light",
|
||||||
|
toggleTheme: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [theme, setTheme] = useState("light");
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
theme,
|
||||||
|
toggleTheme: () => setTheme((current) => (current === "light" ? "dark" : "light"))
|
||||||
|
}),
|
||||||
|
[theme]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
return useContext(ThemeContext);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import App from "./App";
|
||||||
|
import { ThemeProvider } from "./context/ThemeContext";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ThemeProvider>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
function ContactPage() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
message: ""
|
||||||
|
});
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
|
const errors = useMemo(() => {
|
||||||
|
const validationErrors = {};
|
||||||
|
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
validationErrors.name = "A nev megadasa kotelezo.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.email.trim()) {
|
||||||
|
validationErrors.email = "Az email cim megadasa kotelezo.";
|
||||||
|
} else if (!/^\S+@\S+\.\S+$/.test(formData.email)) {
|
||||||
|
validationErrors.email = "Adj meg ervenyes email cimet.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.message.trim().length < 10) {
|
||||||
|
validationErrors.message = "Az uzenet legalabb 10 karakter legyen.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationErrors;
|
||||||
|
}, [formData]);
|
||||||
|
|
||||||
|
const isValid = Object.keys(errors).length === 0;
|
||||||
|
|
||||||
|
function handleChange(event) {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
setFormData((current) => ({ ...current, [name]: value }));
|
||||||
|
setSubmitted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="panel">
|
||||||
|
<h2>Kapcsolat</h2>
|
||||||
|
<p>Kontrollalt urlap peldaval, valos ideju validacioval.</p>
|
||||||
|
|
||||||
|
<form className="contact-form" onSubmit={handleSubmit} noValidate>
|
||||||
|
<label htmlFor="name">Nev</label>
|
||||||
|
<input id="name" name="name" value={formData.name} onChange={handleChange} />
|
||||||
|
{errors.name && <p className="error-message">{errors.name}</p>}
|
||||||
|
|
||||||
|
<label htmlFor="email">Email</label>
|
||||||
|
<input id="email" name="email" value={formData.email} onChange={handleChange} />
|
||||||
|
{errors.email && <p className="error-message">{errors.email}</p>}
|
||||||
|
|
||||||
|
<label htmlFor="message">Uzenet</label>
|
||||||
|
<textarea id="message" name="message" rows="5" value={formData.message} onChange={handleChange} />
|
||||||
|
{errors.message && <p className="error-message">{errors.message}</p>}
|
||||||
|
|
||||||
|
<button type="submit" disabled={!isValid}>
|
||||||
|
Kuldes
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{submitted && <p className="success-message">Sikeres kuldes (demo).</p>}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContactPage;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
function HomePage() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="panel">
|
||||||
|
<h2>React alap mintaalkalmazas</h2>
|
||||||
|
<p>
|
||||||
|
Ez az oldal egy oktatasi frontend, ami bemutatja a komponenseket, allapotkezelest,
|
||||||
|
routingot, contextet, kontrollalt urlapot es API hivasokat.
|
||||||
|
</p>
|
||||||
|
<div className="counter">
|
||||||
|
<p>useState pelda: szamlalo erteke {count}</p>
|
||||||
|
<button onClick={() => setCount((current) => current + 1)}>Novel</button>
|
||||||
|
<button onClick={() => setCount(0)}>Nullaz</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
import { fetchProductById } from "../api/productsApi";
|
||||||
|
|
||||||
|
function ProductDetailsPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const [product, setProduct] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
async function loadProduct() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
const data = await fetchProductById(id);
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setProduct(data);
|
||||||
|
}
|
||||||
|
} catch (requestError) {
|
||||||
|
if (isMounted) {
|
||||||
|
setError("A termek adatainak betoltese nem sikerult.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProduct();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <p>Betoltes folyamatban...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p className="error-message">{error}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return <p>Nincs ilyen termek.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="panel details-page">
|
||||||
|
<Link to="/termekek">Vissza a listahoz</Link>
|
||||||
|
<h2>{product.title}</h2>
|
||||||
|
<img src={product.image} alt={product.title} />
|
||||||
|
<p>Kategoria: {product.category}</p>
|
||||||
|
<p>{product.description}</p>
|
||||||
|
<strong>{product.price.toFixed(2)} USD</strong>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductDetailsPage;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import ProductCard from "../components/ProductCard";
|
||||||
|
import { fetchProducts } from "../api/productsApi";
|
||||||
|
|
||||||
|
function ProductsPage() {
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [cartCount, setCartCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
async function loadProducts() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
const data = await fetchProducts();
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setProducts(data);
|
||||||
|
}
|
||||||
|
} catch (requestError) {
|
||||||
|
if (isMounted) {
|
||||||
|
setError("Az adatok betoltese nem sikerult. Probald ujra kesobb.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProducts();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredProducts = useMemo(() => {
|
||||||
|
return products.filter((product) =>
|
||||||
|
product.title.toLowerCase().includes(searchTerm.trim().toLowerCase())
|
||||||
|
);
|
||||||
|
}, [products, searchTerm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="panel">
|
||||||
|
<h2>Termekek</h2>
|
||||||
|
<p>A kosarban levo termekek szama: {cartCount}</p>
|
||||||
|
|
||||||
|
<label htmlFor="search">Szures nev alapjan</label>
|
||||||
|
<input
|
||||||
|
id="search"
|
||||||
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(event) => setSearchTerm(event.target.value)}
|
||||||
|
placeholder="Pl. backpack"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{loading && <p>Betoltes folyamatban...</p>}
|
||||||
|
{!loading && error && <p className="error-message">{error}</p>}
|
||||||
|
{!loading && !error && filteredProducts.length === 0 && <p>Nincs talalat.</p>}
|
||||||
|
|
||||||
|
<div className="product-grid">
|
||||||
|
{filteredProducts.map((product) => (
|
||||||
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
onAddToCart={() => setCartCount((current) => current + 1)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductsPage;
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
:root {
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(120deg, #f4f7ff, #eef3f6);
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #d6dce5;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-button {
|
||||||
|
border: 1px solid #93c5fd;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e3a8a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid #d6dce5;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter button,
|
||||||
|
.card-actions button,
|
||||||
|
.contact-form button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.55rem 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #0ea5e9;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.65rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-grid {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
border: 1px solid #dbe5ef;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fbff;
|
||||||
|
padding: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card img {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
object-fit: contain;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-page img {
|
||||||
|
width: min(280px, 100%);
|
||||||
|
display: block;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin: 0;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
margin: 0;
|
||||||
|
color: #15803d;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell.dark {
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell.dark .topbar {
|
||||||
|
background: rgba(15, 23, 42, 0.95);
|
||||||
|
border-bottom-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell.dark .panel {
|
||||||
|
background: #1e293b;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell.dark .product-card {
|
||||||
|
background: #1f2937;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell.dark .nav-link {
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell.dark .nav-link.active {
|
||||||
|
color: #fb7185;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.topbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar nav {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()]
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user