8. gyakorlat – Projekt nulláról és shadcn/ui
Bevezetés
Az előző gyakorlatokon előre elkészített projektbe dolgoztunk. Ma az alkalmazást nulláról hozzuk létre — telepítjük a függőségeket, beállítjuk az eszközöket, majd felépítjük a webshopot lépésről lépésre.
A mai óra fő témái:
- VS Code bővítmények — Tailwind IntelliSense, ES7+ snippetek, Prettier
- Projekt bootstrapping — egyetlen parancs:
npx shadcn@latest init --preset b1zBuS --template vite - Adatmodell és state — egyszerű
Product[]kosár, egy helyen kezelt state - Komponensek összerakása —
interface extends, shadcn/ui Card és Button, lucide ikonok
1. VS Code bővítmények
Mielőtt elkezdünk, telepíts három bővítményt, amelyek az óra alatt folyamatosan segítenek:
| Bővítmény | Mire való |
|---|---|
Tailwind CSS IntelliSense (bradlc.vscode-tailwindcss) | Autocompletion és preview a Tailwind osztályokhoz |
ES7+ React/Redux/React-Native snippets (dsznajder.es7-react-js-snippets) | rafce → teljes komponens sablon egy gépelésre |
Prettier (esbenp.prettier-vscode) | Automatikus kódformázás mentéskor |
2. Projekt létrehozása
Egyetlen parancs létrehozza a teljes projektet: Vite + React + TypeScript + TailwindCSS + shadcn/ui, minden konfigurációval:
npx shadcn@latest init --preset b1zBuS --template vite
A --preset egy előre elkészített shadcn konfiguráció (stílus, szín, elrendezés), a --template vite pedig a Vite + React + TypeScript sablont jelöli. A parancs futtatása után azonnal indítható a fejlesztői szerver:
cd webshop
npm run dev
A shadcn/ui nem telepít egy új npm csomagot minden komponenshez — a komponensek forráskódját másolja be a projektedbe. A button.tsx és a card.tsx pontosan olyan saját fájlok, mint a ProductCard.tsx. Módosíthatod, átírhatod őket.
Komponens hozzáadása
Ha egy új shadcn komponensre van szükséged:
npx shadcn@latest add card
Ez létrehozza a src/components/ui/card.tsx fájlt. Az elérhető komponensek listája és dokumentációja: ui.shadcn.com
3. Az adatmodell
// src/data/products.ts
export interface Product {
id: number
name: string
price: number
emoji: string
}
export const products: Product[] = [
{ id: 1, name: 'Mechanical Keyboard', price: 29900, emoji: '⌨️' },
{ id: 2, name: 'Wireless Mouse', price: 12900, emoji: '🖱️' },
{ id: 3, name: 'USB-C Hub', price: 8900, emoji: '🔌' },
{ id: 4, name: 'Monitor Stand', price: 15900, emoji: '🖥️' },
{ id: 5, name: 'Webcam HD', price: 19900, emoji: '📷' },
{ id: 6, name: 'LED Desk Lamp', price: 6900, emoji: '💡' },
]
Ez a modell szándékosan egyszerűbb a gyak7-nél: nincs category, rating, image, description. Csak az abszolút minimum, ami egy termék kártyához kell.
4. Kosár state az App-ban
// src/App.tsx
import { useState } from "react"
import { products, type Product } from "./data/products"
import Navbar from "./components/Navbar"
import ProductCard from "./components/ProductCard"
export function App() {
const [cartItems, setCartItems] = useState<Product[]>([])
function addToCart(id: number): void {
const product = products.find(x => x.id === id)
if (!product) return
setCartItems(state => [...state, product])
}
return (
<div className="min-h-screen bg-background">
<Navbar cartCount={cartItems.length} />
<main className="max-w-5xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Termékek</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{products.map((product: Product) => (
<ProductCard
key={product.id}
id={product.id}
name={product.name}
price={product.price}
emoji={product.emoji}
addToCart={addToCart}
/>
))}
</div>
</main>
</div>
)
}
A grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 osztályok reszponzív CSS Grid elrendezést hoznak létre: mobilon 1 oszlop, tableten 2, asztali gépen 3. A töréspontok (breakpointok) a Tailwind alapértelmezésére épülnek (sm = 640px, md = 768px).
Figyelj oda: Product[] nem CartItem[]
A gyak7-ben a kosár CartItem[] volt, ahol egy elem { product, quantity } struktúrát tartalmazott. Most a kosár egyszerűen Product[] — ha valaki kétszer vesz egy terméket, az kétszer szerepel a tömbben.
Ez azt jelenti:
cartItems.length= összes kosárba tett tétel száma (nem egyedi termékek)- Nincs mennyiség-kezelés, nincs deduplikálás
Egyszerűbb, de korlátozottabb. A modellválasztás mindig kompromisszum: ez a változat gyorsabban megvalósítható.
Az addToCart függvény a products tömböt állomásban (const products = [...]) keresi az id alapján. Ezért a find mindig szinkron és instant — nincs szükség useEffect-re vagy aszinkron logikára.
5. Navbar és ProductCard – props és interface öröklés
Navbar
// src/components/Navbar.tsx
import { Button } from './ui/button'
import { ShoppingCart } from 'lucide-react'
const Navbar = (props: { cartCount: number }) => {
return (
<header className='border-b sticky top-0 bg-background z-10'>
<div className='max-w-5xl mx-auto px-4 h-16 flex items-center justify-between'>
<span>Webshop</span>
<Button>
<ShoppingCart />
Kosár ({props.cartCount})
</Button>
</div>
</header>
)
}
A ShoppingCart ikon a lucide-react csomagból jön — ez egy React komponens, ugyanúgy kell importálni, mint bármelyik másikat. Az összes elérhető ikon neve és előnézete megtalálható a lucide.dev/icons oldalon.
A Button a shadcn/ui button.tsx-ből jön — ez a te kódod a src/components/ui/ mappában.
ProductCard – interface extends
// src/components/ProductCard.tsx
import type { Product } from "@/data/products"
interface ProductCardProps extends Product {
addToCart: (id: number) => void
}
const ProductCard = ({ id, name, price, emoji, addToCart }: ProductCardProps) => {
return (
<Card>
<CardHeader>
<div className="mb-2 text-5xl">{emoji}</div>
<CardTitle className="text-[24px]">{name}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{price} Ft</p>
</CardContent>
<CardFooter>
<Button className="w-full" onClick={() => addToCart(id)}>Kosárba</Button>
</CardFooter>
</Card>
)
}
A ProductCardProps extends Product a TypeScript interface-öröklés: a ProductCardProps tartalmaz mindent, amit a Product tartalmaz (id, name, price, emoji), plusz a saját mezőit (addToCart).
Ezért az App.tsx-ben a ProductCard-nak átad(hat)juk a termék összes mezőjét külön-külön:
<ProductCard
key={product.id}
id={product.id}
name={product.name}
price={product.price}
emoji={product.emoji}
addToCart={addToCart}
/>
Alternatívaként a spread operátor ({...product}) ugyanezt csinálja rövidebben, de a fenti forma átláthatóbb: pontosan látod, mit adunk át.
Összefoglalás
Amit ma építettünk
| Fájl | Szerepe |
|---|---|
data/products.ts | Product interface és statikus adatok |
App.tsx | Kosár state (Product[]) + addToCart, termékek renderelése |
components/Navbar.tsx | cartCount prop, shadcn Button, lucide ShoppingCart ikon |
components/ProductCard.tsx | interface extends Product, shadcn Card + Button |
main.tsx | ThemeProvider becsomagolja az alkalmazást (sötét/világos téma) |
Az interface extends minta
// Alap típus
interface Product {
id: number
name: string
price: number
emoji: string
}
// Kibővített típus: Product minden mezője + addToCart
interface ProductCardProps extends Product {
addToCart: (id: number) => void
}
Akkor használd, ha egy komponens props-ai pontosan tartalmazzák egy meglévő interface mezőit, plusz néhány extras. Elkerüli a duplikálást.
Product[] vs CartItem[]
| Megközelítés | Előny | Hátrány |
|---|---|---|
Product[] (mai) | Egyszerű, length = darabszám | Duplikátumok = mennyiség, nincs quantity mező |
CartItem[] (gyak7) | Explicit mennyiség, könnyen bővíthető | Bonyolultabb addToCart logika |
A mai verzió gyorsabban megvalósítható — a modellválasztás mindig kompromisszum.
Házi feladat
Az órai projektet (webshop-<neptun kódod>) egészítsd ki egy oldalpanellel, amely a kosár tartalmát jeleníti meg.
A feladat
A shadcn/ui Sheet komponens egy kihúzható oldalpanelt biztosít. Add hozzá a projekthez, és kösd össze a Navbar kosár-gombjával úgy, hogy kattintásra megjelenjenek a kosár tételei.
Sheet komponens: shadcn/ui Sheet
Demo oldal: webshop-elte.vercel.app
1. lépés – Sheet telepítése
Az órai projekt mappájában futtasd:
npx shadcn@latest add sheet
Ez létrehozza a src/components/ui/sheet.tsx fájlt.
2. lépés – Navbar módosítása
A Navbar jelenleg cartCount: number prop-ot kap. Cseréld ki cartItems: Product[]-ra — így az összes adat elérhető lesz a panelben.
App.tsx — a Navbar hívásánál:
// régi
<Navbar cartCount={cartItems.length} />
// új
<Navbar cartItems={cartItems} />
Navbar.tsx — importáld a Sheet komponenseit, majd módosítsd a props-t és add hozzá a panelt:
// TODO: importáld a Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger komponenseket
// a '@/components/ui/sheet' útvonalról
import { Button } from '@/components/ui/button'
import { ShoppingCart } from 'lucide-react'
import { Product } from '@/data/products'
interface NavbarProps {
// TODO: cseréld ki cartCount: number helyett cartItems: Product[]-ra
}
const Navbar = ({ cartItems }: NavbarProps) => {
return (
<header className="border-b sticky top-0 bg-background z-10">
<div className="max-w-5xl mx-auto px-4 h-16 flex items-center justify-between">
<span>Webshop</span>
{/* TODO: csomagold a Button-t egy <Sheet> + <SheetTrigger asChild> elembe */}
<Button variant="outline">
<ShoppingCart />
Kosár ({cartItems.length})
</Button>
{/* TODO: add hozzá a <SheetContent>-et:
- <SheetHeader> + <SheetTitle>Kosár</SheetTitle>
- ha a kosár üres, jelenítsd meg: "A kosár üres."
- ha nem üres, listázd a tételeket (emoji, név, ár)
- jelenítsd meg a végösszeget (cartItems.reduce(...)) */}
</div>
</header>
)
}
A SheetTrigger asChild azt jelenti, hogy a trigger nem egy extra <button>-t rak a DOM-ba — a közvetlen gyerekkomponens (itt a Button) maga lesz a megnyitó elem.
3. lépés – Tesztelés
- Indítsd el a projektet:
npm run dev - Kattints néhány termékre, hogy kerüljenek a kosárba
- Kattints a Navbar ikonra — a panelnek ki kell csúsznia jobb oldalon
- Ellenőrizd, hogy a termékek neve, emojija és ára helyesen jelenik meg
- Ellenőrizd, hogy az összeg helyesen számolódik
GitHub regisztráció
A következő órán deployolni fogjuk az alkalmazást. Ehhez szükséges egy GitHub fiók — ha még nincs, most hozd létre.
- Menj a github.com oldalra
- Kattints a Sign up gombra jobb felül
- Add meg az e-mail címedet, válassz felhasználónevet és jelszót
A felhasználóneved megjelenik a projekt URL-jében is (pl. github.com/felhasznalonev/webshop). Válassz olyat, amit szívesen megmutatsz.
- Erősítsd meg az e-mail-t a kapott levélből
- Telepítsd a Git-et, ha még nincs: git-scm.com/downloads
- Terminálban állítsd be a nevedet és az e-mail-ed:
git config --global user.name "Teljes Neved"
git config --global user.email "email@example.com"
Ellenőrzőlista
- A
sheetkomponens telepítve (npx shadcn@latest add sheet) - A
NavbarcartItems: Product[]prop-ot kap (nemcartCount) - Az
App.tsx-ben a Navbar hívása frissítve - A Sheet megnyílik a kosár-gombra kattintva
- Üres kosárnál “A kosár üres.” szöveg látható
- Tételek esetén minden termék neve, emojija és ára megjelenik
- A végösszeg helyesen számolódik
- GitHub fiók létrehozva, Git konfigurálva
Beadás
Canvasen (4. házi feladat) töltsd fel a projekted tömörítve, ELŐTTE TÖRÖLD a node_modules mappát!