react_minta
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user