Skip to content
This repository has been archived by the owner on Apr 21, 2024. It is now read-only.

đŸ›ïž En fullstack e-handel hemsida byggd med Next, Typescript, React Query, Bun, Hono, Drizzle, Docker, Tailwind, ocksĂ„ del av mitt godkĂ€nda gymnasiearbete

Notifications You must be signed in to change notification settings

balazshevesi/clothing-webshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Clothing Webshop đŸ›ïž

Fullstack e-handel hemsida


(Read the English translation 🇬🇧)

Warning

Detta repo innehÄller kÀllkoden till projekt-delen av mitt gymnasiearbete, i och med att det nu Àr godkÀnt sÄ kÀnner jag att den har uppfyllt sitt syfte, dÀrmed har jag arkiverat den

InnehÄll

Tech stack och dependencies

    • Eftersom att jag bygger en webbshop sĂ„ kommer jag behöva bra SEO. Bra SEO Ă€r inte nĂ„got som en standard SPA erbjuder, sĂ„ dĂ€rför blir man tvungen till att antingen server rendera den eller skriva typ rĂ„ HTML. Server rendering lĂ„ter ju trevligare.

      Jag valde att anvÀnda mig av nextjs dÄ det typ Àr det enda sÀttet att server rendera React och samtidigt anvÀnda de nya server komponent mönsterna.

    • Server komponenter Ă€r det sjĂ€lvklara sĂ€ttet att göra server rendering och data fetching pĂ„. Jag anvĂ€nder de sĂ„ mycket jag kan.

    • State management

      • Jag tycker om konceptet med unidirectional data flow och global state som Redux populariserade. Men jag ogillar all setup, boilerplate, och komplexitet som kommer med Redux.

        Jag valde Zustand för att konceptet Àr identiskt till Redux, men implantationen Àr betydligt enklare.

      • Jag valde att anvĂ€nda Tanstack Query i admin panelen för att hantera dels data fetching, och dels caching av data.

      • Jag rĂ„kade hitta Nuqs i en github trĂ„d nĂ€r jag letade information om hur man hanterar URL query params i nextjs appar, och Nuqs visade sig vara den perfekta lösningen. APIn Ă€r exakt som en useState, men staten synkroniseras automagiskt med URL quires. Repot förtjĂ€nar mer stjĂ€rnor

    • Styling

      • I min erfarenhet sĂ„ Ă€r Tailwind det absolut enklaste sĂ€ttet Ă€r göra styling pĂ„.

      • Heroicons brukar vara min go-to för ikoner. De har inte den största urvalet, men alla av ikonerna ser bra ut, och dessutom har de outlineade versioner.

      • Om man redan anvĂ€nder React och Tailwind sĂ„ Ă€r Shadcn ett sjĂ€lvklart val.

        Det som skiljer Shadcn/ui Ät all de andra komponent biblioteken Àr att du sjÀlv Àger Àger komponenterna. Om du vill Àndra nÄgonting pÄ de sÄ kan du helt enkelt bara öppna komponenten och Àndra det sjÀlv.

    • Jag valde Hono för att den har ett API som efterliknar express, men Ă€r kompatibel med Bun runtime och har allmĂ€nt bĂ€ttre prestanda.

    • Jag valde drizzle som min ORM för att APIn efterliknar vanlig SQL.

    • JWT signering och verifiering för att hantera authentication.

    • För att kryptera lösenorden.

  • Jag valde mySql som min databas dels för att lĂ€ra mig nĂ„got nytt och dels för att en e-handels hemsida Ă€r full av relationer, sĂ„ att SQL passar perfekt

  • Övrigt

    • Majoriteten av gĂ„ngerna sĂ„ anvĂ€nder jag inte ens Typescript korrekt 😂, men Ă€ndĂ„ sĂ„ Ă€r det en enorm hjĂ€lp för att förebygga buggar, speciellt pĂ„ backenden, dĂ€r man inte alltid Ă€r sĂ€ker pĂ„ vad alla funktioner returnerar

    • Jag vill inte spendera tid och mental energi pĂ„ att formatera min kod, sĂ„ jag valde att anvĂ€nda prettier (dock Ă€r sidoeffekten att man köttar CMD+S efter typ varje knapp tryck 😂, men det kan jag leva med). Jag anvĂ€nder Import-sort pluginet ifrĂ„n trivago och Tailwind-classname-sort-pluginet, de Ă€r nicee

      Eslint anvÀnder jag helt enkelt med default instÀllningarna som Nextjs kommer med.

    • Inputs behöver valideras, annars sĂ„ kommer anvĂ€ndare kunna skicka all möjligt skit till backenden, det vill vi inte tillĂ„ta.

      Det populÀraste validerings biblioteket Àr nog Zod. Nackdelen med Zod Àr att import storleken Àr (onödigt) stor. Valibot kan ofta ha en import storlek som Àr 10x mindre Àn Zod. Och sÄ föredrar jag Valibots dokumentation.

    • Jag anvĂ€nde Postman mest bara för att kolla formen av min JSON, det Ă€r as nice att ha det pĂ„ andra skĂ€rmen.

    • Jag anvĂ€nde Dbeaver för att enklare kunna hantera och visualisera min databas

    • Jag anvĂ€nder docker för att förenkla hostingen av min Bun backend

Databas design

Bild pÄ databas visualisering ifrÄn Dbeaver Bilden Àr en visualisering av databasen skapad med Dbeaver.

Detta var mina krav pÄ databasen:

  • Kunna sĂ€lja produkter
  • Kunna ha olika mĂ€rken och olika kategorier
  • Kunna sĂ€lja variationer av produkter, typ storlek och fĂ€rg
  • Kunna ha rabatter pĂ„ vissa variationer av produkter, men inte andra
  • Kunna belysa en viss variation av en produkt
  • Kunna ha ha unika bilder pĂ„ varje variation
  • Admins ska kunna se vad alla har i kundvagnarna, tom de som inte Ă€r registrerad

Jag valde att bygga ut hela "produkt" grejen med att tÀnka pÄ varje variation av en produkt som en artikel, och sedan ha anonnser som innehÄller flera artiklar. Annonserna behöver dÀrmed ocksÄ ha nÄgon typ av "default" artikel

Hosting och deployment

Namngivningskonventioner

  • Databas: snake_case

  • API Route namn: kebab-case

  • JS/TS Code: camelCase

  • Client-Side Storage: camelCase

  • Types och Schema validering: PascalCase

  • Environment variabel: SCREAMING_SNAKE_CASE

  • Extra: Databas tabeller ska ha Tbl som suffix

Jag valde dessa conventioner för att simplifiera och streamlina utvecklings processen och samtidigt följa best-practices. Tanken bakom de Àr ju att jag som utvecklare inte ska behöva fundera pÄ triviala grejer som namn givning, samt att man inte ska behöver tÀnka typ "fan, vad heter den endpointen igen?".

Problem, problemlösning och lÀrdomar

Detta projekt vart fullt av lĂ€rdomar för mig. Jag stötte pĂ„ alla sorters problem, allt ifrĂ„n att jag lĂ„ste ut mig sjĂ€lv ifrĂ„n min egen databas, till att jag satt i timmar med en ".Dockerfile", som borde hetat "Dockerfile" 😂.

  • State management

    LĂ€s

    Detta Ă€r faktiskt andra gĂ„ngen jag har försökt att bygga detta för fösta gĂ„ngen sĂ„ blev det kaos pga min state management lösning inte var genomtĂ€nkt. Hela Kundvagnen var lagrad i sin egen komponent som lĂ„g relativt lĂ„ngt in i DOM trĂ€det, sĂ„ det blev vĂ€ldigt svĂ„rt för andra komponenter (som köp-knappen) att komma Ă„t den. Jag insĂ„g det rĂ€tt snabbt att jag borde ha anvĂ€nt mig av (i alla fall) en context run hela skiten. Men hela dev-ex:en (och dĂ€rmed min motivation 😂) hann gĂ„ till bajs innan jag faktiskt bytte den till en context.

    NÀr jag byggde-om den sÄ viste jag ifrÄn första början att jag var tvungen att lösa state management pÄ nÄgot genomtÀnkt men samtidigt simpelt sÀtt. SÄ jag valde att testa Zustand, och det funkar fint tycker jag.

  • Behovet av en ORM

    LĂ€s

    Detta Àr första projektet som jag anvÀnde SQL i. NÀr jag började bygga ut backenden sÄ tÀnkte jag att det skulle gÄ bra med att skriva rÄ SQL. SÄ jag valde att skapa stored procedures, som jag sedan skulle anropa i koden. Jag insÄg snabbt att det var ett vÀldigt dÄligt mönster, för jag var ju tvungen till att anvÀnda parametrized queries (för att skydda mot SQL-injections) och dÄ blev det ju typ 7 rader kod för en enkel CRUD operation (som dessutom inte ens var type-safe), och koden blev vÀldigt svÄrlÀst.

    DÄ fick jag den genialiska idén att abstrahera bort de 7 raderna till sin egen funktion. Sen insÄg jag hur efterblivet det egentligen var; jag hade skapat en helper funktion för varje stored procedure för att förenkla lÀsbarheten av koden, men i processen sÄ gjorde jag det mycket vÀrre. Relativt enkla CRUD-operationer hade sina egna helper funktioner som i sin tur kallade pÄ stored procedures, som i sin tur faktiskt utförde CRUD-operationerna i databasen. Man kan ju inte hÄlla pÄ sÄ om man ska bygga nÄgot underhÄllbart.

    SĂ„ jag valde att utforska lite om vilka alternativ som fanns. Jag hamnade mellan Prisma och Drizzle ORM. BĂ„da verkade vara kompetenta lösningar. Jag rĂ„kade dock radera hela min databas nĂ€r jag försökte insallera Prisma (jag missuppfattade vad "database migration" egentligen syftar pĂ„ 😂), sĂ„ frustrationen ledde mig till đŸ—„ïž Drizzle 😂.

    Jag tycker faktiskt att Drizzle passade bÀttre Àn Prisma. pga att APIn efterliknar vanligt SQL-kod (som jag fösöker bli mer bekant med).

  • Stateless backend och singleton(-ish) design

    LĂ€s

    State i backend Àr ett helt nytt koncept för mig, före detta projektet sÄ tÀnkte jag aldrig ens pÄ det. API ruttarna i Next Àr stateless, i mitt fall sÄ Àr det ett problem eftersom att det betyder att vartenda rutt kommer att göra sin egen anslutning till databasen. DÄ hade jag min databas pÄ RDS som hade en max-anslutning pÄ 60, och nÀr man har Next i dev-mode sÄ kommer anslutningarna inte att disconnecta pÄ hot-realods, sÄ att de 60 anslutningarna fylldes jÀvligt snabbt.

    Varje individuella rutt har ju sin egen state, sÄ först tÀnkte jag att jag kanske skulle kunna utnytja det genom att ha nÄgon typ av intern rutt som returnerar databas anslutnings objektet. Men det visade sig komplexa objekt (som databas anslutningar) inte kunnde skickas genom HTTP :(.

    SjÀlv tycker jag att Next borde ha nÄgon inbyggd lösning pÄ detta, men samtidigt sÄ kommer de ju aldrig göra det med tankte pÄ att de tror att man borde göra typ allt i server-komponenter.

    Lösningen Ă€r ju att man har nĂ„gon typ av "pooling". Prisma har nĂ„tt magiskt rust-lager som hjĂ€lper till med det, men jag valde ju Drizzle 💀. Som tur Ă€r sĂ„ kan man ju ocksĂ„ ha pooling pĂ„ databas-nivĂ„, jag försökte fixa det i min AWS RDS panel, men det ville inte fungera, sĂ„ jag bestĂ€mde mig för att bygga-om min backend med Bun och Hono.

    Motivationen till det var dels ocksÄ att jag började ogilla file-based-routing mer och mer. Jag tycker att file-based-routing fungerar fint pÄ frontenden, men inte pÄ backenden. Motivationen till bygga om den var dels ocksÄ att Next inte har nÄgon riktig middleware lösning för backend rutter, och jag var tvungen att ha typ 10 rader boiler-plate kod i varje "admin/" rutt bara för att checka-av om anropet faktisk kom ifrÄn en admin.

    Rent tekniskt sÄ Àr vÀll anslutningen inte riktigt singelton eftersom jag anvÀnder "mysql.createPool". Jag gör det pga att jag stötte pÄ nÄgon typ av timeout-bug dÀr anslutningen stÀngdes efter nÄgon timme, men det var omöjligt att detektera det (förutom om man vill wrappa varje endpoint i en try-catch, vilket man ju inte vill). mysql.createPool hanterar sÄdana grejer Ät mig.

  • Client-side caching pĂ„ i admin panelen

    LĂ€s

    Första gÄngen jag byggde ut admin panelen sÄ tÀnkte jag att jag skulle anvÀnda server-komponenter, men det visade sig vara ett rÀtt dumt val. Server-komponenter renderas ju pÄ servern, nÀr webblÀsaren tar emot de som cachar den de. Det betyder att trots att innehÄllet kan ha Àndrats sÄ kommer webblÀsaren visa den cachade verisonen och inte be servern efter en ny. I praktiken sÄ betyder det att man kan lÀgga till en artikel i admin/articles/add, och sedan nÀr man kommer tillbaks till admin/articles sÄ kommer den nya artikeln inte visas. Denna chachingen gÄr inte att stÀnga av. Dokumentationen sÀger (komiskt nog) typ bara "nej".

    Pga av att innehÄllet pÄ admin panelen Àr vÀldigt interaktivt sÄ Àr det nog smartare att bygga ut data fetchingen pÄ clienten istÀllet. Jag har aldrig anvÀnt react query innan, men hÀr passar den faktiskt perfekt.

    Bild ifrÄn nextjs dokumentationen

  • Att hosta en bun API

    LĂ€s

    Bun Àr en relativt ny grej och dÀrmed finns det inga bra no-bullshit guider pÄ att hosta det. Efter lite googling sÄ kom jag fram till att jag var tvungen till att kötta ner den i en docker container. Det finns ju nÄn officiala Dockerfile template pÄ Bun:s hemsida, men jag valde att anvÀnda en ifÄn nÄn artikel pÄ Medium för att den verkade mycket mer simpel.

    NÀsta steg blev dÄ att hitta nÄgot system för att hosta dockerfilen. AWS har ju EC2 eller Lambda, men komplexiteten Àr jÀvligt hög, (jag vet inte riktigt hur det hade fungerat, men jag antar) att jag hade först behövt göra nÄgon typ av automatisering som lyssnar pÄ commits pÄ github repot, sen hÀmtar dockerfilen och bygger en docker image ifrÄn den, och sedan hostar den pÄ EC2 eller Lambda. Det lÄter cp-komplicerat, jag ville ha nÄgot mer simpelt.

    Med Render kan man bara koppla github repot och sen bara funkar det, och de verkade stödja docker, men cold-startsen Àr brutala (typ 1min). Senare hittade jag att Railway ocksÄ kunde deploya docker (dÀr Àr cold-startsen helt okej).

  • State initialisering innan hydration

    LĂ€s

    "Login" knappen Àr nÄgonting som Àr beroÀnde av state. Om anvÀndaren Àr inloggad sÄ ska det stÄ "view account", om den inte Àr inloggad sÄ ska det stÄ "login". Staten gÄr att initialisera pÄ clienten med javascript, men om anvÀndaren inloggad sÄ kommer det stÄ "login" innan sidan hydratisera. Det ser konstigt ut, sÄ jag initialiserade staten med en serverkomponent, sedan tar clienten över.

    Lösningen Àr inte 100% optimal eftersom den orsakar en extra rerender, men navigationen Àr en vÀldigt viktig del av UX, sÄ det fÄr man ta.

    Railway app har samma problem, men de har inte löst det hah

Gymnasiearbete

Detta projekt Ă€r en del av mitt godkĂ€nda Gymnasiearbete pĂ„ HaganĂ€sskolan, Älmhult (Teknikprogrammet).

Rapporten finns tillgÀnglig som PDF-fil i detta repo, men det Àr enklast att öppna den med nbviewer.

Sammanfattning (kopierad ifrÄn rapporten)

I detta gymnasiearbete presenteras processen för att skapa en e-handelssida, dÀr jag har utforskat och anvÀnt mig av moderna webbteknologier inom bÄde front-end-end och back- end utveckling. Arbetet inkluderar en översikt över relevanta JavaScript-ramverk, databasval mellan SQL och NoSQL, samt en diskussion kring de tekniska beslut som fattats under projektets gÄng. Slutresultatet Àr en fungerande webbshop, med insikter och reflektioner kring de utmaningar och lÀrdomar projektet medfört.

Galleri

Butikens header Butikens mest populÀra
Header för butikssidan Mest populÀra produkter
Produktsida Sök efter varumÀrke
Produktsida Sök efter varumÀrke
Sökfilter Kundvagn
Sökfilter Kundvagnen
Kassan Admin - Startsida
Kassan Admin - Startsida
Admin - Artiklar Admin - Redigera artiklar
Admin - Artiklar Admin - Redigera artiklar
Admin - VarumÀrken Admin - Redigera varumÀrken
Admin - VarumÀrken Admin - Redigera varumÀrken
Admin - Kategorier Admin - Redigera kategorier
Admin - Kategorier Admin - Redigera kategorier
Admin - Listor Admin - Redigera listor
Admin - Listor Admin - Redigera listor
Admin - Planerade reor Admin - Redigera planerade reor
Admin - Planerade reor Admin - Redigera planerade reor

TODO

prio 1

  • Streamline input validation and form submission across the app
  • Add success screen after payment, and wipe cart items

prio 2

  • Add to orders table after successful payment??
  • Add statistics to admin panel
  • Find email provider and setup forgot password system
  • Fix behavior if only one color/size is available

prio 3

  • probably shouldn't store user info (apart from userId) on the client
  • remove a bunch of unused console logs
  • Make so buyers can submit reviews
  • Ai integration??? like talk with the cart? let the ai modify the cart??

  • fix wierd discount bugg
  • Fix checkout for guest users
  • Make so that the cancel button on the stripe page brings you back to the prevous page that you were on. pretty much just need to add an argument to the goToCheckout function
  • Integrate stripe
  • fix error on editing listing, dunno why
  • Present account info in a cleaner way, and make it editable
  • Add planned sales shit
  • Fix bug with red hoodie,
  • get nav links from the backend, also put in so that the "listing view page" has a link to view the brand, and a link to view the category
  • Build filter/browse section of the website
  • Fix weird (race condition?) bug with cart state syncing (probably caused by incorrect implementation of debouncing, would probably be fixed by removing debouncing entirely)
  • Build search functionality? (dunno how, but i'll find out)
  • FIX BUG where the backend tries to send commands to the database, even though the connection is closed (kinda fixed maybe??)
  • Write some tests? idk
  • Write a nice readme
  • Translate readme

  • Write GA loggbok from commit history
  • Chill