English flagEnglish

8. gyakorlat – Projekt nulláról és shadcn/ui

2026-04-12 5 perc olvasási idő GitHub

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ásainterface 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ényMire 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

// 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ájlSzerepe
data/products.tsProduct interface és statikus adatok
App.tsxKosár state (Product[]) + addToCart, termékek renderelése
components/Navbar.tsxcartCount prop, shadcn Button, lucide ShoppingCart ikon
components/ProductCard.tsxinterface extends Product, shadcn Card + Button
main.tsxThemeProvider 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ésElőnyHátrány
Product[] (mai)Egyszerű, length = darabszámDupliká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

  1. Indítsd el a projektet: npm run dev
  2. Kattints néhány termékre, hogy kerüljenek a kosárba
  3. Kattints a Navbar ikonra — a panelnek ki kell csúsznia jobb oldalon
  4. Ellenőrizd, hogy a termékek neve, emojija és ára helyesen jelenik meg
  5. 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.

  1. Menj a github.com oldalra
  2. Kattints a Sign up gombra jobb felül
  3. 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.

  1. Erősítsd meg az e-mail-t a kapott levélből
  2. Telepítsd a Git-et, ha még nincs: git-scm.com/downloads
  3. 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 sheet komponens telepítve (npx shadcn@latest add sheet)
  • A Navbar cartItems: Product[] prop-ot kap (nem cartCount)
  • 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!