Noir · Kraków Coffee Atelier
Kraków Coffee Atelier — interactive lifestyle app
Kraków Coffee Atelier — interactive lifestyle app
Miejsca z segmentu specialty coffee mają specyficzny problem: ich oferta jest złożona — ziarna różnią się profilem sensorycznym, metodą przetwarzania, wysokością uprawy — a standardowy sklep lub strona-wizytówka tego nie komunikuje. Noir rozwiązuje to przez interaktywny Flavor Explorer: użytkownik kalibruje cztery osie smakowe (czekolada, cytrus, ferment, floral) i w czasie rzeczywistym dostaje posortowane rekomendacje ziaren, odzwierciedlające jego preferencje. Oprócz tego aplikacja obsługuje rezerwacje stolików, preorder paczek w trzech wariantach oraz moduł "Pochodzenie" — edytorski content oparty na plikach Markdown zarządzanych przez Nuxt Content.
Stack technologiczny
Nuxt 3 z Vue 3 Composition API
Framework zapewnia file-based routing (każda podstrona to oddzielny plik .vue w katalogu pages/), SSR/SSG gotowy pod Cloudflare Pages oraz useSeoMeta() dla każdej trasy z osobna. Całość działa bez osobnego serwera backendowego.
TypeScript 5 z rygorystycznym typowaniem domenowym
W types/index.ts zdefiniowano 7 interfejsów (CoffeeBean, Event, Booking, PreorderItem, OriginStory, Package, FlavorProfile). Kluczowe pola używają union types zamiast string — np. processing: 'Washed' | 'Natural' | 'Honey' | 'Anaerobic' | 'Anaerobic Honey'. Błąd typowy jest niemożliwy do przekazania do formularza rezerwacji.
Nuxt Content v3
Treści edytorskie (origin stories czterech regionów kawowych) przechowywane jako pliki .md z frontmatterem YAML. Pozwala na aktualizację opisów bez dotykania kodu Vue — wystarczy edycja pliku tekstowego.
Composable useFlavorFilter
Logika filtrowania ziaren wyekstrahowana do reużywalnego composable opartego na reactive() i computed(). Algorytm oblicza dystans Manhattan między profilem użytkownika a profilem każdego ziarna w 4-wymiarowej przestrzeni smakowej, filtruje wyniki z progiem diff <= 12 i sortuje rosnąco. Całość reaktywna — żadnego ręcznego watch.
Composable useReservation
Walidacja pól przez iterację po tablicy (keyof Booking)[], co eliminuje ryzyko pominięcia pola przy rozbudowie interfejsu. Wystarczy dodać klucz do interfejsu Booking — walidator automatycznie go obejmie.
Tailwind CSS z rozszerzonym design tokenem
tailwind.config.ts definiuje semantyczną paletę (espresso, dark, surface, surface2, brass, cream) zamiast surowych kolorów. Zmiana całej palety projektu wymaga edycji jednego pliku.
Najciekawsze rozwiązania techniczne
System animacji oparty na IntersectionObserver z CSS-only fallbackiem
Instancja IntersectionObserver z progiem threshold: 0.12 obserwuje wszystkie elementy z klasą .reveal. W momencie wejścia w viewport dodaje klasę .in, co wyzwala transition CSS (opacity + translateY z cubic-bezier(.16,1,.3,1) — krzywa spring). Elementy po wejściu są od razu unobserve-owane — zero zbędnych callbacków. Staggering przez klasy .delay-1 do .delay-5 eliminuje potrzebę biblioteki animacji.
Magnetyczny efekt przycisków bez zewnętrznej biblioteki
Przyciski z klasą .mag-btn rejestrują mousemove i obliczają offset kursora względem środka elementu przez getBoundingClientRect(). Przesunięcie skalowane współczynnikami 0.18 (oś X) i 0.25 (oś Y) daje subtelny efekt przyciągania zamiast dosłownego podążania. Przy mouseleave transform jest zerowany — CSS transition: transform obsługuje powrót.
Kursor z interpolacją liniową (lerp) przez requestAnimationFrame
Dwa elementy kursora: kropka (reaguje natychmiastowo) i ring (podąża z opóźnieniem). Ring porusza się przez pętlę requestAnimationFrame z wzorem rx += (mx - rx) * 0.12 — interpolacja liniowa z tłumieniem 0.12. Efektem jest płynne, "wlokące się" zachowanie ringa sprawiające wrażenie fizycznej inercji bez żadnej biblioteki motion.
Podsumowanie
Projekt pokazuje świadome decyzje architektoniczne: logika reusowalna trafia do composables, model danych jest typowany union typami zamiast luźnymi stringami, animacje działają przez natywne API przeglądarki (IntersectionObserver, requestAnimationFrame, CSS transitions) bez dodatkowych zależności w package.json.
Twoja firma może być
moim kolejnym projektem
Umów 30-minutową rozmowę — opowiesz mi o firmie, a ja powiem, jak zbudowałbym dla Ciebie podobne rozwiązanie.
Bez prezentacji sprzedażowej, bez zobowiązań