Saleor Store
우리 컬렉션을 쇼핑하세요
Epiloges Fashion Boutique는 크레타 섬 헤라클리온에 위치한 의류 매장입니다
Το Epiloges Fashion Boutique είναι κατάστημα ρούχων στο Ηράκλειο Κρήτης. Γυναικεία και ανδρικά ενδύματα, με προσοχή στη λεπτομέρεια και διαχρονικό σχεδιασμό.
커머스TypeScript · JavaScriptGitHub @epiloges온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기E commerce fashion boutique
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A luxury footwear ecommerce shop — Zara/COS-inspired minimal editorial design — live in production at shopalexandris.vercel.app, selling a real catalogue and taking real orders.
Storefront, checkout, payments, transactional email, courier integration and a full admin dashboard are all real and Postgres-backed. It is built on a vendor-neutral commerce abstraction, so a different backend (Shopify, WooCommerce, Medusa, Commerce Layer) could be swapped in by writing adapters rather than rewriting the app.README 전문 보기 ↗A luxury footwear ecommerce shop — Zara/COS-inspired minimal editorial design — live in production at shopalexandris.vercel.app, selling a real catalogue and taking real orders.
Storefront, checkout, payments, transactional email, courier integration and a full admin dashboard are all real and Postgres-backed. It is built on a vendor-neutral commerce abstraction, so a different backend (Shopify, WooCommerce, Medusa, Commerce Layer) could be swapped in by writing adapters rather than rewriting the app.
Two things are deliberately not connected yet, and the shop is honest about both rather than faking them: card payments (the Piraeus Bank / Euronet epay integration is built and unit-tested against the bank's specification, but the e-commerce activation on the acquiring contract has not been issued yet, so checkout offers Cash on Delivery and Bank Transfer only) and outbound email (no EMAIL_PROVIDER is set, so mail is written to the EmailLog table and never delivered).
Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS v4 · shadcn/ui (Base UI) · Prisma 7 + Neon Postgres · Framer Motion · React Hook Form + Zod · next-intl · Vercel Blob · Resend · Vitest
npm install
Copy .env.example to .env and fill it in. DATABASE_URL (a Neon pooled connection string), ADMIN_SESSION_SECRET and CUSTOMER_SESSION_SECRET are the minimum needed to boot; everything else degrades gracefully when unset, which is the point — an unconfigured provider hides its UI or falls back to a safe local default rather than erroring.
npx prisma migrate deploy
npx tsx scripts/seed.ts
npm run dev
Open http://localhost:3000 for the storefront, or http://localhost:3000/admin for the dashboard.
Admin access. Authentication is real: an AdminUser row in Postgres, a bcrypt-hashed password, and a jose-signed JWT session cookie enforced by proxy.ts.
There are no demo credentials and nothing is pre-filled on the login form. scripts/seed.ts creates the first admin from SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD, and if those are unset it generates a random password and prints it once:
Created admin user [email protected].
Password: kW3f9Qx7_pR2vNc1LtYbHs4A
This is shown ONCE and is not stored anywhere in plaintext.
This used to be a hardcoded admin123 exported from lib/auth.ts and published here. In a public repository that is a working login to the account that can edit prices, read customer addresses and delete orders — it only had to be seeded once against a real database to become real.
Re-running the seed never touches an existing admin. After the first one, manage admins at /admin/users (create, delete, self-service password change, with guards against deleting yourself or the last remaining admin).
npx tsx scripts/check-launch-placeholders.ts
Fails loudly if placeholder content is still in the repo. It exists because a "This Is a Demo" Privacy Policy section and an unreachable .example contact address both survived on the live storefront long enough to stop being noticed — the failure mode is that nobody looks.
Nothing renders hardcoded content. Every component is prop-driven and reads through one seam:
Postgres → the source of truth (Prisma, prisma/schema.prisma)
data/*.json → FALLBACKS only, used when a SiteContent row is missing,
and the seed input for scripts/seed.ts
services/*.ts → content + domain functions (products, collections, nav,
homepage, blog, settings, search, payments)
lib/commerce/ → the vendor-neutral commerce abstraction —
Cart, Checkout, Customer, Wishlist, Search, Auth, Analytics
components/ → ui/ (shadcn primitives), layout/, sections/, product/,
cart/, admin/, shared/
types/ → the domain model (Product, Collection, HomepageSection
as a discriminated union, etc.)
The data/*.json files are fallbacks, not content. The running shop reads the database. Editing one of them changes nothing a visitor sees unless the corresponding SiteContent row is missing — which is exactly how a demo domain once survived in every canonical URL while the repo looked correct. The scripts/apply-*.ts scripts exist to write these values into the live rows.
lib/commerce/types.ts defines ten vendor-neutral service interfaces (ProductService, CollectionService, CartService, CheckoutService, CustomerService, WishlistService, SearchService, CMSService, AuthenticationService, AnalyticsService) aggregated into one CommerceProvider. No component ever imports a vendor SDK. lib/commerce/index.ts is the single factory (getCommerceProvider()), and it is the only switch statement in the codebase that knows adapters exist.
Eight of the ten — Products, Collections, Cart, Checkout, Customer, Wishlist, Auth and Search — are Postgres-backed through Route Handler facades, because this factory only ever runs in the browser and so cannot import Prisma-backed services directly. Only CMS and Analytics remain locally implemented. The exported name createMockCommerceProvider is legacy from when all ten were mocks; renaming it touches every import site and has been deferred.
Dependency injection throughout: SearchService takes ProductService as a constructor argument rather than reaching into storage itself.
See PAYMENTS.md for the full guide. Checkout → payment abstraction → selected provider. The checkout knows only PaymentMethod / PaymentIntent / PaymentStatus / PaymentResult and takes exactly one behavioural branch (customerAction.type === "redirect"), which is about the action, not the vendor. Adding a provider is a new file plus one registry line.
awaiting_bank_transfer → processing edge in its state machine: creating the order does not mark it paid, because there is no automatic path to settlement.Provider credentials are AES-256-GCM encrypted at rest with a key derived from PAYMENTS_CONFIG_SECRET; a missing key is a hard error at the point of use rather than a silent fallback to a hardcoded default. Every provider can also be configured entirely through environment variables (<PROVIDER_ID>_<FIELD_KEY>, upper-snake-cased), which is the right choice in production — an env var always wins over an admin-saved value, and the admin shows such a field as read-only rather than pretending a write took effect.
Payment status is tracked separately from order status, with an append-only payment_transactions audit trail and a single server-side assertTransition chokepoint.
/checkout is a multi-step guest checkout (contact → shipping → delivery → payment → review). Orders, carts and checkouts are all persisted in Postgres. VAT is inclusive at the Greek 24% — vatIncludedIn() is gross × rate / (1 + rate), not gross × rate; confusing those two is a real bug this codebase has already had, which is why it is one named function. VAT is computed on the pre-gift-card total, because a gift card is a means of payment rather than a price reduction.
Delivery estimates skip Greek public holidays, including the Orthodox Easter cluster.
Customers get real email/password auth plus Google, Facebook and Apple OAuth (lib/oauth/). Each provider's button simply doesn't render until its credentials are set, so an unconfigured provider degrades rather than erroring. Sessions are signed JWTs in cookies, with CUSTOMER_SESSION_SECRET deliberately separate from ADMIN_SESSION_SECRET so compromising one doesn't compromise the other. Password reset works end to end (request → emailed token → single-use redemption).
/account/login and /account/register are public; the rest of /account is behind a route-group layout that redirects signed-out visitors. /wishlist stays guest-accessible.
Storefront search and all listing-page filtering run in Postgres (services/search.ts, /api/search). One request returns a page of results, the facet counts and the price bounds together.
It uses raw SQL rather than the query builder for a specific reason: every price rule operates on the effective price, COALESCE(salePrice, price), which Prisma cannot express in where/orderBy — and almost every product carries a sale price. Facets count the scope, not the refined set, so picking one colour doesn't zero every other. Every sort carries p.id as a tiebreaker; without a total order, equal-priced products swap between pages and one is never seen.
The header overlay is instant search — debounced live queries, inline product and collection previews, keyboard navigation. There is no /search results page, so results are not currently linkable or shareable.
/women, /men, /new-in, /sale, /collections and /collections/[slug] share one template, components/plp/ProductListingPage.tsx: colour/size/price/availability filters with faceted counts, sort and infinite scroll, all driven by the URL (?color=&size=&minPrice=&sort=&page=) so any filtered view is a shareable link.
/new-in sorts by when a product was actually added rather than filtering on a badge, so it stays correct on its own as stock arrives.
types/product.ts covers multi-image galleries, product video, per-size inventory (quantity, SKU, barcode), preorder/backorder, gender/season/materials/care, related products and an SEO override. lib/product.ts centralises derived logic (getEffectivePrice, getProductBadges, isSizePurchasable) so pricing and badge rules live in exactly one place.
Scarcity badges count sizes remaining, not total units. This shop carries roughly one pair per size, so counting units made the badge fire on 90% of the catalogue, which tells a shopper nothing. "Last size" and "Few sizes left" are also distinguished, because they are different messages to someone deciding whether to buy now.
There is no "N people bought this recently" indicator. There was one; it was a hash of the SKU rather than a fact, so it was deleted rather than flagged off.
Postgres-backed, with promo codes, gift cards, automatic VAT and free-shipping-threshold calculation, saved-for-later, recommendations and stock-aware quantity limits. CartDrawer and /cart share one useCart() hook.
/admin covers products, a variant editor (stock is tracked per size, not per colour×size), inventory, collections and categories, discounts, gift cards, orders, customers, returns, analytics, the homepage sections builder with a real draft/publish workflow, hero management, a media library with real uploads to Vercel Blob, navigation, blog posts, SEO and site settings, and users. Everything persists to Postgres.
Roles are enforced, not decorative: constants/permissions.ts holds 13 keyed capabilities used by both the UI and the server-side guard on every mutation. Orders and inventory page, search and filter in SQL rather than shipping the whole table to the browser.
Renaming a category records its outgoing slug in the same transaction, so old URLs 308 rather than 404 — the redirect lives in proxy.ts, not the page, because permanentRedirect in a streaming context emits a client-side meta refresh rather than a real 308.
Stock is restored when an order is cancelled or refunded, claimed via a null-guarded Order.restockedAt so a re-save cannot double-credit. Returns do not yet restock — that is a separate path needing its own per-item claim column.
All follow the same pattern: an interface, a safe default, and a real implementation that activates only once credentials exist.
| Integration | Default | Real |
|---|---|---|
dev — writes to EmailLog, sends nothing | Resend | |
| Courier | manual — admin types a tracking number | ACS Courier |
| Image storage | disabled, with a clear "not configured" error | Vercel Blob |
| OAuth | button hidden | Google, Facebook, Apple |
Rate limiting on write endpoints and the catalogue query. Real security response headers, with no unsafe-eval in the production CSP, plus base-uri, form-action and object-src. Branded error.tsx / global-error.tsx / not-found.tsx. A structured lib/logger.ts. Cookie consent with a real gate.
Do not add a loading.tsx to any route that calls notFound(). A Suspense boundary commits HTTP 200 before notFound() can run, which turned every missing product, category, collection and legal page into a soft 404 across the whole app. The root loader is scoped to an app/(listing)/ route group covering only pages that cannot 404.
Metadata API with per-page generateMetadata, JSON-LD (Organization with real trader identity + WebSite site-wide; Product and Breadcrumb on product pages), app/robots.ts, app/sitemap.ts, app/manifest.ts, app/opengraph-image.tsx.
robots.txt and sitemap.xml are statically prerendered — they bake database values at build time, so changing the site URL in the database needs a redeploy before they update. Canonical tags are dynamic and change immediately.
The site URL lives in three places that must agree: the live SiteContent "seo" row, data/seo.json as its fallback, and NEXT_PUBLIC_SITE_URL for links inside emails sent from contexts with no incoming request. scripts/apply-site-url.ts handles the database half.
data/legal.json is generated by scripts/rewrite-legal.ts from constants/company.ts, so the registered trader identity exists in exactly one place and cannot drift between the footer, the contact page, three legal documents and the structured data. The documents are written for a Greek distance seller: lawful bases, processors, retention, GDPR rights and the Hellenic DPA, the 14-day statutory withdrawal right alongside the shop's own 30-day returns policy, the ODR platform and the Consumer's Ombudsman.
legalName and brandName are deliberately separate fields — legal documents must name the trader as registered, since using the trading name in a Privacy Policy defeats the point of naming the data controller.
npm test # vitest, 186 tests
npx tsc --noEmit
npx eslint
By design: card payments (Piraeus epay needs the bank's credentials, not code), outbound email (Resend needs a key), real multi-currency conversion (totals are currency-aware but only ever computed in EUR), restocking on returns, and full Greek localisation — messages/el.json covers the UI strings but there are no locale URLs or hreflang, so only one language is indexable.
See PROGRESS.md for the full build log and NOTES.md for the current session's state.