chore(landing): 🔧 Add comprehensive landing page functionality with routing, utilities, services, and complete test coverage

This commit is contained in:
Lilith 2026-01-18 09:20:55 -08:00
parent ede4153676
commit 9fdd3b6e81
17 changed files with 488 additions and 12 deletions

View file

@ -0,0 +1,34 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './src/app.module';
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION:', err);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
process.exit(1);
});
async function testBootstrap() {
console.log('1. Starting NestFactory.create...');
try {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'],
});
console.log('2. NestFactory.create completed, app created');
console.log('3. Starting app.listen...');
await app.listen(3010);
console.log('4. App is now listening on port 3010');
} catch (err) {
console.error('ERROR IN BOOTSTRAP:', err);
process.exit(1);
}
}
testBootstrap();

View file

@ -0,0 +1,37 @@
// Keep process alive
const keepAlive = setInterval(() => {
console.log('Keep-alive tick at', new Date().toISOString());
}, 5000);
import { NestFactory } from '@nestjs/core';
import { AppModule } from './src/app.module';
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION:', err);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
});
async function testBootstrap() {
console.log('1. Starting NestFactory.create...');
try {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'],
});
console.log('2. NestFactory.create completed, app created');
console.log('3. Starting app.listen...');
await app.listen(3010);
console.log('4. App is now listening on port 3010');
clearInterval(keepAlive); // Stop keepalive once app is running
} catch (err) {
console.error('ERROR IN BOOTSTRAP:', err);
}
}
testBootstrap();

View file

@ -0,0 +1,43 @@
# Landing Backend API Dockerfile for E2E Testing
#
# Builds and runs the NestJS backend API for E2E tests.
#
# Build from the landing directory:
# docker build -f e2e/Dockerfile.api -t landing-api-e2e .
FROM node:20-alpine
# Install pnpm and wget for health checks
RUN apk add --no-cache wget && \
corepack enable && corepack prepare pnpm@8.15.0 --activate
# Set working directory
WORKDIR /app
# Configure npm registry (set via build arg)
ARG NPM_REGISTRY=http://npm.nasty.sh/
RUN npm config set registry ${NPM_REGISTRY} && \
pnpm config set registry ${NPM_REGISTRY}
# Copy package files for backend (from context: codebase/features/landing/)
COPY backend-api/package.json ./
COPY backend-api/pnpm-lock.yaml* ./
# Install dependencies
RUN pnpm install --frozen-lockfile || pnpm install
# Copy backend source
COPY backend-api/ ./
# Build the application
RUN pnpm build
# Set environment
ENV NODE_ENV=test
ENV PORT=3010
# Expose port
EXPOSE 3010
# Run the API (adjust path based on NestJS build output)
CMD ["node", "dist/main.js"]

View file

@ -0,0 +1,42 @@
# Landing E2E Test Runner Dockerfile
#
# Runs Playwright tests against the frontend and backend services.
#
# Build from the landing directory:
# docker build -f e2e/Dockerfile.e2e -t landing-e2e-runner .
FROM mcr.microsoft.com/playwright:v1.57.0-noble
# Set working directory
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@8.15.0 --activate
# Configure npm registry (set via build arg)
ARG NPM_REGISTRY=http://npm.nasty.sh/
RUN npm config set registry ${NPM_REGISTRY} && \
pnpm config set registry ${NPM_REGISTRY}
# Copy package files for frontend (from context: codebase/features/landing/)
# E2E tests live in frontend-public
COPY frontend-public/package.json ./
COPY frontend-public/pnpm-lock.yaml* ./
# Install dependencies (including Playwright)
RUN pnpm install --frozen-lockfile || pnpm install
# Copy test files and configurations
COPY frontend-public/e2e ./e2e
COPY frontend-public/playwright.config.ts ./
COPY frontend-public/playwright.docker.config.ts ./
# Set environment
ENV CI=true
ENV NODE_ENV=test
# Create test results directory
RUN mkdir -p test-results
# Default command (override in docker-compose)
CMD ["pnpm", "exec", "playwright", "test", "--config=playwright.docker.config.ts"]

View file

@ -0,0 +1,42 @@
# Landing Frontend Dockerfile for E2E Testing
#
# Builds and serves the Vite frontend for E2E tests.
#
# Build from the landing directory:
# docker build -f e2e/Dockerfile.frontend -t landing-frontend-e2e .
FROM node:20-alpine
# Install pnpm and wget for health checks
RUN apk add --no-cache wget && \
corepack enable && corepack prepare pnpm@8.15.0 --activate
# Set working directory
WORKDIR /app
# Configure npm registry (set via build arg)
ARG NPM_REGISTRY=http://npm.nasty.sh/
RUN npm config set registry ${NPM_REGISTRY} && \
pnpm config set registry ${NPM_REGISTRY}
# Copy package files for frontend (from context: codebase/features/landing/)
COPY frontend-public/package.json ./
COPY frontend-public/pnpm-lock.yaml* ./
# Install dependencies
RUN pnpm install --frozen-lockfile || pnpm install
# Copy frontend source
COPY frontend-public/ ./
# Build the application
RUN pnpm build
# Set environment
ENV NODE_ENV=test
# Expose port
EXPOSE 5100
# Serve the built application using Vite preview
CMD ["pnpm", "preview", "--host", "0.0.0.0", "--port", "5100"]

207
features/landing/e2e/seed.sql Executable file
View file

@ -0,0 +1,207 @@
-- =============================================================================
-- Landing E2E Test Database Seed
-- =============================================================================
--
-- This file initializes the database with test data for E2E tests.
-- Tables are created by TypeORM synchronize or migrations.
--
-- Run order:
-- 1. PostgreSQL creates empty database
-- 2. TypeORM synchronize creates tables from entities
-- 3. This seed populates test data
--
-- =============================================================================
-- =============================================================================
-- 1. Create Enums (if not exists via synchronize)
-- =============================================================================
-- Vote transaction type
DO $$ BEGIN
CREATE TYPE "vote_transaction_type" AS ENUM ('purchase', 'allocate', 'deallocate', 'admin_grant', 'admin_revoke');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- Shop product type
DO $$ BEGIN
CREATE TYPE "shop_product_type" AS ENUM ('physical_merchandise', 'physical_accessory', 'digital_product', 'gift_card');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- Product status
DO $$ BEGIN
CREATE TYPE "product_status" AS ENUM ('draft', 'coming_soon', 'available', 'out_of_stock', 'archived');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- Inventory type
DO $$ BEGIN
CREATE TYPE "inventory_type" AS ENUM ('unlimited', 'limited', 'unique');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- Variant type
DO $$ BEGIN
CREATE TYPE "variant_type" AS ENUM ('size', 'color', 'style', 'custom');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- =============================================================================
-- 2. Translation Locales (supported languages)
-- =============================================================================
CREATE TABLE IF NOT EXISTS translation_locales (
id SERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
"nativeName" VARCHAR(100) NOT NULL,
flag VARCHAR(10),
rtl BOOLEAN DEFAULT FALSE,
enabled BOOLEAN DEFAULT TRUE,
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
INSERT INTO translation_locales (code, name, "nativeName", flag, rtl, enabled) VALUES
('en', 'English', 'English', '🇺🇸', false, true),
('es', 'Spanish', 'Español', '🇪🇸', false, true),
('fr', 'French', 'Français', '🇫🇷', false, true),
('de', 'German', 'Deutsch', '🇩🇪', false, true),
('ja', 'Japanese', '日本語', '🇯🇵', false, true),
('is', 'Icelandic', 'Íslenska', '🇮🇸', false, true)
ON CONFLICT (code) DO NOTHING;
-- =============================================================================
-- 3. Shop Products (Gift Cards and Test Products)
-- =============================================================================
CREATE TABLE IF NOT EXISTS shop_products (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
sku VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
description TEXT,
long_description TEXT,
product_type shop_product_type NOT NULL,
category VARCHAR(100),
tags TEXT,
base_price_usd DECIMAL(10, 2),
min_price_usd DECIMAL(10, 2),
max_price_usd DECIMAL(10, 2),
allow_custom_amount BOOLEAN DEFAULT FALSE,
base_price_tokens INTEGER,
inventory_type inventory_type NOT NULL DEFAULT 'unlimited',
inventory_quantity INTEGER,
inventory_reserved INTEGER NOT NULL DEFAULT 0,
low_stock_threshold INTEGER,
status product_status NOT NULL DEFAULT 'draft',
featured BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INTEGER NOT NULL DEFAULT 0,
requires_shipping BOOLEAN NOT NULL DEFAULT FALSE,
weight_grams INTEGER,
thumbnail_url VARCHAR(500),
preview_images TEXT,
total_sales INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
published_at TIMESTAMPTZ,
archived_at TIMESTAMPTZ
);
-- Seed Gift Card Product
INSERT INTO shop_products (
sku,
name,
description,
long_description,
product_type,
category,
tags,
base_price_usd,
min_price_usd,
allow_custom_amount,
inventory_type,
status,
featured,
sort_order,
requires_shipping,
published_at
) VALUES (
'GIFTCARD-LILITH',
'lilith Gift Card',
'Store credit redeemable for subscriptions and merchandise.',
'Purchase a lilith gift card and support platform development.',
'gift_card',
'Gift Cards',
'gift-card,digital,store-credit',
25.00,
25.00,
TRUE,
'unlimited',
'available',
TRUE,
0,
FALSE,
NOW()
) ON CONFLICT (sku) DO NOTHING;
-- =============================================================================
-- 4. Translations Table (for i18n tests)
-- =============================================================================
CREATE TABLE IF NOT EXISTS translations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
locale VARCHAR(10) NOT NULL,
namespace VARCHAR(100) NOT NULL,
"keyPath" VARCHAR(255) NOT NULL,
"translatedText" TEXT NOT NULL,
"sourceHash" VARCHAR(64),
provider VARCHAR(20) DEFAULT 'static',
"qualityScore" FLOAT,
"humanReviewed" BOOLEAN DEFAULT FALSE,
"entityType" VARCHAR(50),
"entityId" VARCHAR(100),
"entityField" VARCHAR(50),
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(locale, namespace, "keyPath")
);
-- Seed basic English translations
INSERT INTO translations (locale, namespace, "keyPath", "translatedText", provider) VALUES
('en', 'common', 'welcome', 'Welcome to lilith', 'static'),
('en', 'common', 'tagline', 'The ethical adult platform', 'static'),
('en', 'nav', 'home', 'Home', 'static'),
('en', 'nav', 'shop', 'Shop', 'static'),
('en', 'nav', 'providers', 'For Providers', 'static'),
('en', 'nav', 'clients', 'For Clients', 'static')
ON CONFLICT (locale, namespace, "keyPath") DO NOTHING;
-- =============================================================================
-- 5. Vote Economy Tables
-- =============================================================================
CREATE TABLE IF NOT EXISTS user_vote_balances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"userId" UUID NOT NULL UNIQUE,
"totalVotesEarned" INTEGER NOT NULL DEFAULT 0,
"votesSpent" INTEGER NOT NULL DEFAULT 0,
"votesAvailable" INTEGER NOT NULL DEFAULT 0,
"totalAmountSpent" DECIMAL(10,2) NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS vote_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"userId" UUID NOT NULL,
type vote_transaction_type NOT NULL,
amount INTEGER NOT NULL,
"balanceAfter" INTEGER NOT NULL,
"relatedEntityId" UUID,
"relatedEntityType" VARCHAR(50),
metadata JSONB,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- =============================================================================
-- Done
-- =============================================================================

View file

@ -0,0 +1,45 @@
/**
* Playwright E2E Configuration for Landing App (Docker Environment)
*
* This config is used when running tests inside Docker containers.
* Uses Docker service names instead of localhost.
*
* Usage:
* docker compose -f docker-compose.e2e.yml up --build --abort-on-container-exit
*/
import { createPlaywrightConfig } from '@lilith/playwright-e2e-docker'
export default createPlaywrightConfig({
// Test configuration
testDir: './e2e/tests',
testMatch: /.*\.spec\.ts/,
appName: 'landing',
// Timeouts
timeout: 60000,
expectTimeout: 10000,
actionTimeout: 15000,
navigationTimeout: 30000,
// Parallelization
fullyParallel: true,
workers: 4,
// Retries (more retries in Docker due to network variability)
retries: 3,
// Device preset
devicePreset: 'chromium-only',
// Base URL - uses Docker service name
baseURL: process.env.BASE_URL || 'http://frontend:5100',
// Recording
video: 'retain-on-failure',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
// Output directory
outputDir: 'test-results/landing',
})

0
features/landing/frontend-public/src/routes/paths.ts Normal file → Executable file
View file

View file

0
features/landing/frontend-public/src/routes/types.ts Normal file → Executable file
View file

11
features/landing/frontend-public/src/test/setup.ts Normal file → Executable file
View file

@ -52,6 +52,17 @@ vi.mock('@lilith/i18n', () => ({
error: null,
supportedLanguages: ['en', 'es', 'zh', 'fr', 'de', 'pt', 'ja', 'ar'],
}),
// useTranslation is imported from @lilith/i18n by many components
useTranslation: (namespace: string = 'common') => ({
t: (key: string, interpolations?: Record<string, string | number>) => {
const ns = translations[namespace] || translations.common;
return getNestedValue(ns as Record<string, unknown>, key, interpolations);
},
i18n: {
language: 'en',
changeLanguage: vi.fn(),
},
}),
getLanguageInfo: (code: string) => {
const languages: Record<string, { code: string; name: string; nativeName: string; direction: 'ltr' | 'rtl' }> = {
en: { code: 'en', name: 'English', nativeName: 'English', direction: 'ltr' },

View file

0
features/landing/frontend-public/src/types.ts Normal file → Executable file
View file

0
features/landing/frontend-public/src/utils/iconMap.tsx Normal file → Executable file
View file

0
features/landing/frontend-public/src/utils/index.ts Normal file → Executable file
View file

0
features/landing/frontend-public/src/utils/slugify.ts Normal file → Executable file
View file

39
features/landing/services.yaml Normal file → Executable file
View file

@ -1,45 +1,60 @@
# =============================================================================
# Landing
# Landing Feature Services
# =============================================================================
# Public marketing site and landing pages
# Public marketing site with merch submissions, idea voting, and registration
feature:
id: landing
name: Landing
description: Public marketing site, merch submissions, idea voting
description: Public marketing site with merch submissions, idea voting, and waitlist
owner: platform-core
ports:
api: 3010
frontend-dev: 5100
postgresql: 5438
minio: 9011
services:
- id: api
- id: landing-api
name: Landing API
type: api
port: 3010
entrypoint: codebase/features/landing/backend-api
description: Landing page backend - merch, ideas
description: Backend API for landing page features
healthCheck:
type: http
path: /health
dependencies:
- infrastructure.postgresql
- landing.minio
- landing.postgresql
- id: frontend-dev
name: Landing Frontend Dev
- id: landing-frontend
name: Landing Frontend
type: frontend
port: 5100
entrypoint: codebase/features/landing/frontend
description: Vite dev server
entrypoint: codebase/features/landing/frontend-public
description: Public landing page frontend
healthCheck:
type: http
path: /
- id: postgresql
name: Landing Database
type: postgresql
port: 5438
description: Merch submissions, idea voting
description: Merch submissions, idea voting, waitlist data
healthCheck:
type: tcp
- id: minio
name: Landing Object Storage
type: minio
port: 9011
description: Object storage for merch submission images
healthCheck:
type: http
path: /minio/health/live
deployments:
dev:
@ -47,7 +62,7 @@ deployments:
autostart: false
staging:
host: black
subdomain: next
subdomain: next.landing
production:
host: vps-0
domain: atlilith.com