react_minta

This commit is contained in:
magdo
2026-03-18 00:01:04 +01:00
parent 28bd7661f5
commit 8b8c08be1b
15 changed files with 655 additions and 0 deletions
+30
View File
@@ -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
+12
View File
@@ -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>
+21
View File
@@ -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"
}
}
+22
View File
@@ -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);
}
+16
View File
@@ -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;
+210
View File
@@ -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;
}
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()]
});