# VIshop — tworzenie własnego szablonu itemshopu (dokument dla AI) Ten dokument jest przeznaczony dla AI (Claude Code, Codex, Cursor, Copilot, Windsurf itp.) budujących szablon itemshopu opartego o VIshop. Zawiera pełny publiczny kontrakt API oraz sprawdzone konwencje z oficjalnych szablonów. Komunikuj się z użytkownikiem w jego języku. ## Czym jest szablon VIshop Szablon to samodzielna strona internetowa (hostowana przez właściciela sklepu), która renderuje itemshop Minecraft/FiveM w całości z publicznego API VIshop na podstawie `shop_id`. Wszystkie dane sklepu — nazwa, logo, nawigacja, serwery/tryby, produkty, ceny, widgety, podstrony — pochodzą z API; w szablonie lokalnie trzyma się tylko branding i konfigurację. ## Zanim napiszesz kod — zadaj użytkownikowi pytania Zbierz od użytkownika następujące decyzje (nie zakładaj ich samodzielnie): 1. **`shop_id`** (widoczny w panelu VIshop) oraz gra: Minecraft czy FiveM. 2. **Sposób płatności** — zawsze daj wybór: - **VIshop Pay** — zero własnego checkoutu, jeden skrypt + jedna funkcja (lightbox) albo link do zewnętrznej strony płatności, - **płatności na stronie** — własny formularz zakupu, pełna kontrola nad wyglądem, więcej pracy. Szczegóły i tabela decyzyjna w sekcji „Płatności" niżej. 3. **Technologia** — zawsze daj wybór: Nuxt 3 + Pinia + Tailwind (domyślna, tak są zbudowane oficjalne szablony) albo cokolwiek innego (Next, Astro, Vite + vanilla JS…) — API to zwykły REST z otwartym CORS, działa z każdym stackiem. 4. **Funkcje** (wielokrotny wybór): widget ostatnich zakupów, najbogatsi gracze, pasek celu miesięcznego, strona voucherów, kody rabatowe (tylko przy płatnościach na stronie), ogłoszenia, podstrony + nawigacja z panelu, lista administracji, licznik graczy online (mcsrvstat), slidery ilości. 5. **Kierunek designu**: kolory, klimat, strony referencyjne. ## Konwencje (z oficjalnych szablonów) - Cała edytowalna przez użytkownika konfiguracja w JEDNYM miejscu (Nuxt: `nuxt.config.js` → `runtimeConfig.public`): `shop_id`, adres serwera, opis, link Discord, social media, lista administracji, kolory. Reszta pochodzi z API. - Baza API: `https://dev123.vishop.pl/panel/shops/{shop_id}/`. Mimo nazwy sugerującej środowisko deweloperskie to JEST produkcyjny host API (używa go oficjalna dokumentacja i wszystkie oficjalne szablony). Trzymaj bazowy URL w jednym miejscu (composable / store / helper), nie kopiuj po plikach. - Theming: zmienne CSS (`--primary-color` itp.) zasilane z konfiguracji, żeby właściciel mógł przemalować sklep bez ruszania komponentów. - Gating funkcji: renderuj widget tylko gdy flagi sklepu na to pozwalają (`shop.latest_payments`, `shop.richest_player`, `shop.monthly_goal_public !== null`) — endpointy wyłączonych widgetów zwracają 403. - Skiny graczy: `https://minotar.net/helm/{nick}/64`, `https://mc-heads.net/body/{nick}/110`. Status serwera: `https://api.mcsrvstat.us/2/{adres}`. - Wdrożenie: Dockerfile (`node:22-alpine`, aplikacja na :3000) + nginx reverse-proxy w docker-compose, albo ręcznie `npm install && npm run build && npm run start` za nginx. Udokumentuj oba sposoby w README wygenerowanego projektu. Pełny poradnik instalacji: https://wiki.vishop.pl/szablony/ - Teksty UI domyślnie po polsku (taka jest społeczność VIshop); zapytaj, jeśli użytkownik chce inny język lub i18n. ## Twarde zasady - **Stopka to wymóg licencyjny.** Każdy szablon MUSI mieć widoczną stopkę z linkiem do VIshop, np. `Strona jest zasilana przez itemshop Minecraft VIshop.pl`. Usunięcie lub ukrycie jej łamie licencję i skutkuje blokadą sklepu. Umieść nad nią komentarz ostrzegawczy w kodzie. - **Sanityzuj cały HTML z API** przed wstrzyknięciem (`product.description`, `subpage.content`, `announcement.content`). - **Nigdy nie hardkoduj „zł".** Każdą kwotę formatuj walutą `shop.currency` (PLN / EUR / USD / UAH) z `GET /panel/shops/{id}/`. - Końcowe ukośniki (trailing slash) w URL-ach API są wymagane. --- # Publiczne API — referencja Baza: `https://dev123.vishop.pl` — wszystkie ścieżki sklepu pod `/panel/shops/{shop_id}/`. Tylko JSON. **Wymagane końcowe ukośniki.** CORS otwarty — można wołać bezpośrednio z przeglądarki. **Autoryzacja:** każdy GET poniżej działa bez uwierzytelnienia. Dwie rzeczy są **objęte wymogiem premium przy wywołaniu z zewnętrznej domeny** (origin spoza domen VIshop): `GET /servers/` oraz `POST /products/{id}/payments/` → 403, jeśli sklep nie ma aktywnego premium. Reszta działa też dla darmowych sklepów. ## Spis endpointów | Metoda | Ścieżka (pod `/panel/shops/{shop_id}`) | Cel | |---|---|---| | GET | `/` | Informacje o sklepie + ustawienia + ogłoszenia | | GET | `/servers/` | Lista serwerów/trybów (premium z zewnętrznej domeny) | | GET | `/servers/{id}/` | Pojedynczy serwer | | GET | `/products/?server={id}` | Produkty serwera (`name=` wyszukiwanie, `simple=1` okrojony format) | | GET | `/products/{id}/` | Pojedynczy produkt | | GET | `/payments/` | Operatorzy płatności skonfigurowani w sklepie | | POST | `/products/{id}/payments/` | Utworzenie płatności — sekcja „Płatności" | | GET | `/payment-status/{payment_uuid}/` | Status transakcji | | GET | `/latest_payments/?amount=N` | Ostatnie zakupy (domyślnie 5, max 15) | | GET | `/richest_player/?amount=N` | Najbogatsi gracze (1–3) | | GET | `/announcements/` | Ogłoszenia (są też osadzone w obiekcie sklepu) | | GET | `/subpages/{id}/` | Podstrona CMS | | GET | `/navigation/` | Pozycje menu (też osadzone w obiekcie sklepu) | | POST | `/codes/use/` | Podgląd kodu rabatowego | | POST | `/vouchers/use/` | Realizacja vouchera | | GET | `/login_steam/` | FiveM: URL przekierowania Steam OpenID | | GET | `/steam_callback/?...` | FiveM: callback → `{uid, username}` | Poza zakresem sklepu: `GET /panel/domain/{domena}/` → goły integer z id sklepu (szablon na własnej domenie może tak ustalić swój `shop_id`). ## Sklep — `GET /panel/shops/{shop_id}/` Publiczne pola: ``` id, name, owner, game, theme, logo, navigation, style, primary_color, home_link, online, latest_payments, richest_player, full_color, rules, ml_widget, sd_widget, premium_until, premium, announcements, monthly_goal_public, domain, richest_player_since, currency ``` - `game`: `"minecraft"` | `"fivem"` — wpływa na walidację nicku i flow Steam - `theme`: `"light"` | `"dark"`; `primary_color`: `#rrggbb`; `style`: surowy CSS ustawiony przez właściciela - `online`: bool — gdy `false`, wyłącz kupowanie (API odrzuca tworzenie płatności komunikatem `"This shop is offline"`) - `latest_payments`, `richest_player`: bool — przełączniki widgetów; gdy wyłączone, endpoint widgetu zwraca 403 - `monthly_goal_public`: procent 0–100 realizacji celu miesięcznego albo `null`, gdy cel nie jest ustawiony - `currency`: `"PLN" | "EUR" | "USD" | "UAH"` — **wszystkie ceny w API to gołe liczby; każdą kwotę formatuj tą walutą** - `rules`: link/treść do checkboxa akceptacji regulaminu (renderuj checkbox tylko gdy ustawione) - `navigation[]`: `{id, shop, type: "redirect"|"subpage", name, url, subpage, subpage_name}` — `subpage` → link wewnętrzny `/subpage/{id}`, w przeciwnym razie zewnętrzny `url` - `announcements[]`: osadzone, ten sam format co endpoint ogłoszeń ## Serwery — `GET /servers/` ```json [{ "id": 1, "name": "Survival", "shop": 1, "ip": "mc.przyklad.pl", "image": "https://...", "hidden": false }] ``` Kolejność wg ustawień właściciela. Ukryte serwery są wykluczone z listy (patrz „Ukryte elementy"). ## Produkty — `GET /products/?server={server_id}` ``` id, name, description, short_description, server, image, main_price, require_player_online, slider, slider_min, slider_max, slider_name, promo, order, hidden, prices ``` - `description`: HTML (sanityzuj przed renderowaniem); `short_description`: zwykły tekst, ≤255 znaków - `promo`: procent 1–99 albo `null` — cena końcowa = `cena × ilość × (1 − promo/100)` - `slider`: gdy `true`, kupujący wybiera `quantity` z przedziału `[slider_min, slider_max]`, z etykietą `slider_name` (domyślnie „Ilość"); gdy `false`, serwer wymusza ilość 1 - `prices`: obiekt z kluczem per operator, wartość = cena u tego operatora albo `null`, gdy produkt nie jest sprzedawany tą metodą: ``` icehost, ivhost, skillhost, paypal, hotpay_sms, hotpay_transfer, hotpay_paysafecard, cashbill_transfer, cashbill_paysafecard, cashbill_paypal, paybylink_transfer, paybylink_paysafecard, paybylink_sms, simpay_directbilling, simpay, dpay, stripe, paymentic, custom ``` UWAGA: dla operatorów SMS (`hotpay_sms`, `paybylink_sms`) wartość **nie jest kwotą** — to id obiektu numeru SMS; rozwiąż je w `sms_numbers` operatora (niżej). `?simple=1` zwraca okrojony format: `{id, name, server: {…}, slider_max}`. ## Operatorzy płatności — `GET /payments/` ```json [{ "id": 7, "name": "Przelew", "provider": "cashbill_transfer", "sms_content": null, "is_sms": false, "sms_numbers": [{ "id": 3, "number": "7255", "price": "3.00", "sms_content": "...", "payment_provider": 7 }] }] ``` - `name` = etykieta przycisku ustawiona przez właściciela; `is_sms` = slug kończy się na `_sms` - Budowanie listy metod dla produktu: `providers.filter(p => product.prices[p.provider] != null)` - Cena/numer/treść SMS: `provider.sms_numbers.find(n => n.id === product.prices[provider.provider])` ## Status płatności — `GET /payment-status/{payment_uuid}/` ```json { "player": "test", "status": "executed", "product_name": "VIP", "quantity": 1, "server": 1, "created_at": "..." } ``` `status`: `waiting` (nieopłacona) → `executing` (opłacona, oczekuje na realizację) → `executed` (zrealizowana); również `canceled`. Traktuj **opłacona = `status !== 'waiting'`**. 404, gdy id nie należy do tego sklepu. ## Ostatnie zakupy — `GET /latest_payments/?amount=5` Tablica obiektów w formacie statusu płatności (tylko opłacone i widoczne, od najnowszych). `amount` domyślnie 5, max 15. Błędy: 403 `"This widget is disabled"`, 400 `"Incorrect amount"` / `"Max amount is 15"`. ## Najbogatsi gracze — `GET /richest_player/?amount=1` `amount=1` (domyślnie) → pojedynczy obiekt `{"player": "...", "spend": "234.86"}`; `amount=2|3` → tablica. Pusty sklep → 200 z pustym body (obsłuż to). 403, gdy widget wyłączony. ## Ogłoszenia — `GET /announcements/` ```json [{ "id": 12, "shop": 34, "color": "#ff0000", "content": "
HTML ≤5000 znaków
" }] ``` Max 5 na sklep. `color` to kolor akcentu/ramki. Osadzone też w obiekcie sklepu — zwykle wystarczy jeden `GET /panel/shops/{id}/`. Sanityzuj `content`. ## Podstrony — `GET /subpages/{id}/` ```json { "id": 495, "name": "Regulamin", "content": "