Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 

Repository files navigation

SSBW — Tienda Prado Impresiones

A full-stack web application replicating part of the Tienda Prado online store. The project is split into three parts:

  • ssbw-api — Express + Node.js backend with REST API, PostgreSQL database, and server-rendered pages
  • ssbw-react — React + Vite + Tailwind CSS frontend SPA
  • ssbw-astro — Astro static site with React islands and Static Site Generation (Tailwind + DaisyUI)

Requirements

  • Node.js >= 23
  • Docker and Docker Compose
  • npm

Project Structure

SSBW/
├── ssbw-api/                        # Express backend
│   ├── docker-compose.yml           # PostgreSQL container (development)
│   ├── docker-compose-prod.yml      # Production stack: db + app + Caddy
│   ├── Dockerfile                   # Containerizes the Express app
│   ├── Caddyfile                    # Reverse proxy configuration
│   ├── .dockerignore                # Excludes node_modules, .env, logs, generated
│   ├── .env                         # Environment variables (not committed)
│   ├── index.ts                     # Express server
│   ├── logger.ts                    # Winston logger configuration
│   ├── package.json
│   ├── tsconfig.json
│   ├── test-api.http                # REST Client test file
│   ├── productos.json               # Scraped product data
│   ├── imagenes/                    # Downloaded product images
│   ├── logs/
│   │   ├── info.log                 # Info and above log entries
│   │   └── error.log                # Error log entries
│   ├── prisma/
│   │   ├── schema.prisma            # Database schema
│   │   ├── prisma.client.ts         # Prisma client configuration
│   │   └── usuario.ts               # Password hashing and authentication methods
│   ├── generated/
│   │   └── prisma/                  # Auto-generated Prisma client
│   ├── routes/
│   │   ├── productos.ts             # Product controllers (MVC)
│   │   ├── usuarios.ts              # Authentication controllers
│   │   └── api.ts                   # RESTful API controllers
│   ├── types/
│   │   └── session.d.ts             # Express session and request type declarations
│   ├── views/
│   │   ├── base.njk                 # Base template with cart offcanvas
│   │   ├── portada.njk              # Home and search page
│   │   ├── detalle.njk              # Product detail page
│   │   └── login.njk                # Login page
│   └── scripts/
│       ├── scrap-tp.js              # Web scraper (Playwright)
│       ├── seed.ts                  # Database seeder
│       └── registra_usuarios.ts     # User registration script
│
└── ssbw-react/                      # React frontend
    ├── index.html
    ├── package.json
    ├── vite.config.ts
    ├── tsconfig.json
    └── src/
        ├── main.tsx                 # Entry point
        ├── App.tsx                  # Main component with router and tabs
        ├── index.css                # Tailwind CSS + DaisyUI + Google Fonts
        ├── components/
        │   ├── Perritos.tsx         # Random dog image (useState + useEffect)
        │   └── Cuadros.tsx          # Random store image (SWR)
        └── pages/
            ├── Portada.tsx          # Home page
            ├── Tarea9.tsx           # Perritos + Cuadros gallery page
            └── Carrousel.tsx        # Swiper product carousel page

ssbw-astro/                          # Astro static site with React islands
    ├── astro.config.mjs
    ├── package.json
    ├── tsconfig.json
    ├── data/
    │   └── productos.json           # Local product data for SSG (no API dependency)
    ├── public/
    │   └── images/                  # Product images served statically
    └── src/
        ├── styles/
        │   └── global.css           # Tailwind CSS + DaisyUI + Google Fonts + font-garamond theme
        ├── layouts/
        │   ├── Layout.astro         # Shared layout (Task 11 pages)
        │   └── LayoutPrado.astro    # Shared layout styled like the real Tienda Prado header
        ├── components/
        │   ├── Welcome.astro        # Astro home page component
        │   ├── Carrousel.tsx        # React island — Swiper carousel (fetches from API)
        │   ├── CarrouselSSG.tsx     # React island — Swiper carousel (receives products as props)
        │   └── CardProducto.astro   # Static product card linking to its detail page
        └── pages/
            ├── index.astro          # url /                    serves Welcome
            ├── carrousel.astro      # url /carrousel            serves CarrouselSSG with local data
            ├── ssg.astro            # url /ssg                  "Destacados" — 12 featured products
            └── productos/
                └── [slug].astro     # url /productos/{título}   dynamic product detail page (getStaticPaths)

Setup

Backend (ssbw-api)

1. Install dependencies

cd ssbw-api
npm install

2. Configure environment variables

Create a .env file inside ssbw-api:

POSTGRES_USER=yo
POSTGRES_PASSWORD=una_clave_muy_segura_123
POSTGRES_DB=ssbw
DATABASE_URL="postgresql://yo:una_clave_muy_segura_123@localhost:5432/ssbw?schema=public"
LOG_LEVEL=debug
SECRET_KEY=your_secret_key_here

3. Start the database

docker compose up -d

4. Run database migrations

npx prisma migrate dev --name esquema_inicial
npx prisma generate

5. Seed the database

npm run seed

6. Register test users

npm run registra

7. Start the backend server

npm run dev

Backend runs at http://localhost:3000


Frontend (ssbw-react)

1. Install dependencies

cd ssbw-react
npm install

2. Start the frontend server

npm run dev

Frontend runs at http://localhost:5173


Astro site (ssbw-astro)

Requires Node.js >= 22.

1. Install dependencies

cd ssbw-astro
npm install

2. Start the Astro dev server

npm run dev

Astro site runs at http://localhost:4321

Make sure the backend (ssbw-api) is also running, since the Carrousel page fetches products from it.


Tasks

Task 1 — Express + Node.js Server

Sets up the base web server using:

  • Express — web framework
  • Nunjucks — templating engine
  • Node.js 23 — native TypeScript support, watch mode, .env file support
npm run dev

Task 2 — Web Scraping

Scrapes all products from Tienda Prado Impresiones using Playwright.

Install browsers (first time only):

npx playwright install

Run the scraper:

npm run scrap

Task 3 — Database with ORM (Prisma + PostgreSQL)

Uses Prisma ORM with PostgreSQL (running in Docker).

The Producto model:

  • id — auto-increment primary key
  • título — product title (max 127 chars)
  • descripción — full description (text)
  • precio — decimal price
  • imagen — image filename (max 127 chars)
npm run seed              # Seed database
npx prisma migrate reset  # Reset database
npx prisma studio         # Open visual DB browser

Task 4 — Home Page, Search and Product Detail Pages

MVC pattern with Prisma as Model and Nunjucks as View.

Route Description
GET / Home page — all products grid
GET /producto/:id Product detail page
GET /buscar?busqueda= Search by title or description

Task 5 — Logger and Shopping Cart

Logger — Winston with three transports:

Transport Level Output
Console debug Colored output with timestamps
File info logs/info.log in JSON format
File error logs/error.log in JSON format

Cart — Server-side sessions with express-session:

Route Description
POST /al-carrito/:id Add product to cart

Task 6 — Authentication

JWT tokens stored in httpOnly cookies. The Usuario model:

  • email — primary key
  • nombre — display name
  • contraseña — bcrypt hashed password
  • admin — boolean role flag

Test users:

Email Password Admin
admin@prado.es admin123 Yes
user@prado.es user123 No
Route Description
GET /login Show login page
POST /login Handle login form
GET /logout Clear cookie and redirect

Task 7 — RESTful API

Method Route Description
GET /api/productos Get all products (pagination + sorting)
GET /api/productos/:id Get product by id
POST /api/productos Create product
PUT /api/productos/:id Update product
DELETE /api/productos/:id Delete product
GET /api/random Get a random product

Pagination:

GET /api/productos?desde=0&hasta=20&ordenación=ascendente

Test with VSCode REST Client extension using test-api.http.


Task 8 — Login UX Improvements and Cart Offcanvas

Login UX (views/login.njk):

  • Autofocus on email field
  • Specialized mobile keyboard (inputmode="email")
  • Show/hide password toggle
  • Email validation on blur
  • Better button text

Cart Offcanvas — Bootstrap offcanvas sliding panel:

  • Opens when clicking the cart icon
  • Uses HTML <template> + cloneNode() for dynamic rendering
  • Fetches cart data via fetch() API
  • Remove items with trash button
Method Route Description
GET /api/carrito Get cart items with product details
DELETE /api/carrito/:id Remove item from cart

Task 9 — SPA with Vite, React and Tailwind

A separate React frontend (ssbw-react) that connects to the Express backend API.

Stack:

  • Vite + React + TypeScript
  • Tailwind CSS with Montserrat and EB Garamond fonts
  • SWR for data fetching

Components:

<Perritos /> — Fetches a random dog image from dog.ceo using useState + useEffect. Click ¡Otro! 🐶 to load a new one.

<Cuadros /> — Fetches a random product image from the store's /api/random endpoint using SWR. Click ¡Otro! 🎨 to load a new one via mutate().

CORS is configured on the Express backend to allow requests from http://localhost:5173.

Both servers must run simultaneously:

# Terminal 1 — backend
cd ssbw-api && npm run dev

# Terminal 2 — frontend
cd ssbw-react && npm run dev

Open http://localhost:5173 to see the gallery.


Task 10 — React Router, DaisyUI Tabs and Swiper Carousel

Expands the React frontend into a multi-page SPA using client-side routing.

Stack added:

  • react-router-dom for client-side routing
  • daisyui for the tabs navigation component
  • swiper for the image carousel

Pages:

Route Page Description
/ Portada.tsx Home / welcome page
/tarea9 Tarea9.tsx Perritos and Cuadros gallery from Task 9
/carrousel Carrousel.tsx Swiper carousel showing real products

Navigation — DaisyUI tabs tabs-boxed component built with NavLink from React Router, highlighting the active tab based on the current route.

Carousel — Fetches a batch of products from GET /api/productos?desde=0&hasta=12 and renders them in a Swiper slider with navigation arrows, pagination dots, autoplay, and responsive breakpoints (1/2/3 slides depending on screen width).


Task 11 — Astro Framework

A new static site (ssbw-astro) built with Astro, using the Islands Architecture to embed an interactive React component inside an otherwise static page.

Stack added:

  • Astro (requires Node.js >= 22)
  • Tailwind CSS for Astro (npx astro add tailwind)
  • DaisyUI for the tabs navigation component
  • @astrojs/react integration
  • Swiper (reused inside the React island)

Structure:

File Description
layouts/Layout.astro Shared HTML structure with Google Fonts, global CSS, and DaisyUI tab navigation
components/Welcome.astro Static Astro component for the home page (no JavaScript shipped)
components/Carrousel.tsx React component (the "island") — same Swiper carousel logic as ssbw-react, fetching products from the Express API
pages/index.astro url / — serves Welcome
pages/carrousel.astro url /carrousel — serves the Carrousel island
pages/ssg.astro url /ssg — placeholder for Task 12 (Static Site Generation)

Astro Islands — By default Astro ships zero JavaScript to the browser. The Carrousel component only becomes interactive because it's explicitly hydrated with the client:load directive:

<Carrousel client:load />

This means the home page (Welcome.astro) loads as pure static HTML, while only the carousel page loads the React/Swiper JavaScript needed for its slider, navigation, and autoplay.

CORS — The Express backend's CORS configuration was updated to also allow requests from http://localhost:4321 (the Astro dev server), in addition to http://localhost:5173 (the React dev server):

app.use(cors({
  origin: ['http://localhost:5173', 'http://localhost:4321'],
  credentials: true
}))

Task 12 — Static Site Generation with Astro

Extends ssbw-astro into a fully static site (SSG) with featured products and individual product pages, no longer dependent on the Express backend at runtime.

Local data instead of API callsproductos.json and the product images were copied locally into the Astro project (data/productos.json and public/images/), so pages are generated entirely at build time:

mkdir ssbw-astro/data
Copy-Item ssbw-api/productos.json ssbw-astro/data/productos.json
Copy-Item -Recurse ssbw-api/imagenes ssbw-astro/public/images

CarrouselSSG.tsx — same Swiper carousel as before, but receiving productos as a prop instead of fetching them:

<CarrouselSSG productos={productos} client:load/>

LayoutPrado.astro — a layout styled closer to the real Tienda Prado header (uppercase EB Garamond logo, top bar, tab navigation), used by the home page, "Destacados" page, and product detail pages.

ssg.astro — "Destacados" page — shows the first 12 products from productos.json in a responsive grid using the CardProducto.astro static component:

Route Description
/ssg Grid of 12 featured products, styled like Tienda Prado's home page

Dynamic product detail pagespages/productos/[slug].astro uses getStaticPaths() to pre-generate one static HTML page per product at build time:

export async function getStaticPaths() {
   return productos.map((pr) => ({
      params: { slug: pr.título.trim() },
      props: { producto: pr }
   }))
}
Route Description
/productos/{título} Static detail page for an individual product (image, full description, price)

Note: the link (href) and the route's params.slug both use the plain, unencoded product title — letting the browser encode it once when navigating and Astro decode it once when matching the route. Encoding it manually on both ends causes a double-encoding mismatch and 404s.

Build and preview the static site:

npm run build
npm run preview

The generated static site (dist/ folder) can then be deployed by uploading it to a static host like Netlify.


Task 13 — IaaS Deployment on a Virtual Private Server

Adds a production-ready Docker Compose stack for ssbw-api, so the whole application (app + database + reverse proxy) can be deployed to any VPS or CaaS with a single command.

Stack — three services:

Service Image Role
db postgres:16-alpine PostgreSQL database
tienda-prado built from local Dockerfile the Express application
caddy caddy:alpine reverse proxy — only service exposed to the host (port 80)

Dockerfile — builds the app on node:24-alpine, installs only production dependencies (npm ci --omit=dev), generates the Prisma client, and starts the app with npx tsx index.ts.

Caddyfile — forwards every request to the app container by its Docker Compose service name:

:80 {
  handle_path /* {
    reverse_proxy tienda-prado:3000
  }
  log {
    output stdout
  }
}

If :80 is replaced with a real domain name, Caddy automatically provisions and renews HTTPS certificates (via Let's Encrypt) with zero extra configuration.

Run the production stack locally:

docker compose -f docker-compose-prod.yml up --build

Then apply migrations, seed the database, and register test users inside the running container:

docker compose -f docker-compose-prod.yml exec tienda-prado npx prisma migrate deploy
docker compose -f docker-compose-prod.yml exec tienda-prado npx tsx scripts/seed.ts
docker compose -f docker-compose-prod.yml exec tienda-prado npx tsx scripts/registra_usuarios.ts

Visit http://localhost (port 80, no port number needed) — only Caddy is published to the host; tienda-prado and db stay reachable only inside Docker's internal network.

Gotchas fixed along the way (useful if reproducing this setup):

  • tsx and dotenv must be in dependencies, not devDependenciesnpm ci --omit=dev strips devDependencies, breaking both the Prisma client (which needs tsx, not plain node, to run) and prisma.config.ts (which imports dotenv/config)
  • ENV instructions in the Dockerfile must come before RUN npx prisma generate, since Prisma needs DATABASE_URL already set at that point
  • A literal $ in an ENV value (e.g. inside SECRET_KEY) must be escaped as $$, or Docker tries to substitute it as a variable
  • Database credentials are passed via docker-compose-prod.yml's environment: section (sourced from .env) instead of hardcoded in the Dockerfile, so the app and db always agree
  • .env itself must be excluded via .dockerignore — otherwise Prisma's client can pick it up at runtime and override the container's DATABASE_URL
  • imagenes/ must not be excluded via .dockerignore — the app serves product images directly from disk

Available Scripts (ssbw-api)

Script Command Description
npm run dev tsx --watch --env-file=.env index.ts Start server in watch mode
npm run scrap node --env-file=.env scripts/scrap-tp.js Run web scraper
npm run seed npx tsx --env-file=.env scripts/seed.ts Seed database
npm run registra npx tsx --env-file=.env scripts/registra_usuarios.ts Register test users

Technologies

Technology Purpose
Node.js 23 Runtime with native TypeScript support
Express Web framework
Nunjucks HTML templating engine
Playwright Web scraping
PostgreSQL Relational database
Prisma 7 ORM
Docker Database container
tsx TypeScript script runner
Bootstrap 5 CSS framework (backend views)
Winston Logging
express-session Session management
jsonwebtoken JWT authentication
bcrypt Password hashing
cookie-parser Cookie middleware
cors Cross-Origin Resource Sharing
React 18 Frontend UI library
Vite Frontend build tool
Tailwind CSS Utility-first CSS framework
DaisyUI Tailwind component library (tabs)
React Router Client-side routing
Swiper Carousel/slider component
SWR React data fetching library
Astro Static site framework with Islands Architecture
@astrojs/react Astro integration for React components
Caddy Reverse proxy with automatic HTTPS
Docker Compose Multi-container production orchestration

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages