Skip to content

Repository files navigation

LinuxAbi Eesti

Arvutiabi, Linuxile üleminek ja kontrollitud kasutatud arvutid – üle Eesti.

LinuxAbi Eesti is a nationwide Estonian Web2 platform concept that connects ordinary computer users with independent local helpers, and offers inspected refurbished business laptops with Linux Mint preinstalled. It also contains a small community knowledge area where visitors can read and ask questions.

The editorial voice and every user-facing string are Estonian. The audience is not Linux enthusiasts: it is people moving away from Windows, people who need general computer help, and people looking for an affordable reliable laptop.


⚠️ This is a demo

This repository is a fully functional demonstration, not a live service.

  • Every provider, review, question, laptop, price, warranty and transaction is fictional mock data. Locations are real Estonian cities; the businesses and people are invented.
  • Nothing is transmitted. Forms validate and store their input only in the visitor's own browser (localStorage). No email is sent, no request reaches a provider, no order is placed and no payment is processed.
  • There is no backend, no database, no accounts and no analytics.
  • Every form explicitly tells the visitor that the action was a demo action.
  • A global demo notice is always visible until the visitor dismisses it.

Do not present this data as real businesses, and do not deploy it publicly without keeping the demo notice in place.


Technology stack

Area Choice
Framework Next.js 16.3.5 (App Router, Turbopack, output: "export")
UI React 19, TypeScript strict mode
Styling Tailwind CSS 4 (CSS-first @theme tokens in src/app/globals.css)
Icons lucide-react
Map Leaflet 1.9 + React Leaflet 5, OpenStreetMap-compatible tiles
Unit tests Vitest (jsdom)
End-to-end tests Playwright (system Chrome channel)

No proprietary backend, no paid APIs and no API keys are required.


Prerequisites

  • Node.js 20.9+ (Node 22 LTS recommended)
  • npm 10+
  • Optional: Docker for container deployment
  • Optional: Google Chrome installed locally for the Playwright e2e suite (the config uses the chrome channel instead of downloading a browser)

Installation

npm install
cp .env.example .env.local   # optional – defaults work without it

Development

npm run dev          # http://localhost:3000

Build & static preview

The build exports every mock route to out/. That folder is the whole application: Cloudflare Pages serves it directly and no Node process is required at runtime. npm run start is only a local preview helper for the exported files (clean URLs and the exported 404 page):

npm run build
npm run start                 # http://localhost:3000
PORT=8080 npm run start       # custom port
HOSTNAME=127.0.0.1 npm run start

Quality commands

npm run typecheck    # next typegen + tsc --noEmit
npm run lint         # eslint .
npm run test         # vitest run (unit tests)
npm run test:e2e     # playwright test (builds and starts the app automatically)
npm run check        # typecheck + lint + test + build
npm run art:laptops  # regenerate the local laptop SVG artwork

npm run check is the gate used before considering a change complete. The Playwright suite is separate because it is slower: it builds the app and drives a real browser. It contains two specs:

  • e2e/demo-flows.spec.ts — the key visitor flows (homepage search, provider filters and profiles, demo service request, laptop filtering/detail/inquiry, Q&A, creating demo content, mobile navigation, 404).
  • e2e/accessibility.spec.ts — automated WCAG 2.2 A/AA checks (axe-core) on every page. All checked pages currently report zero violations.

If Chrome is not installed, use the bundled browser instead:

npx playwright install chromium
PLAYWRIGHT_CHANNEL= npm run test:e2e

To iterate against an already running server:

npm run build && PORT=3100 npm run start
PLAYWRIGHT_SKIP_WEBSERVER=1 E2E_BASE_URL=http://127.0.0.1:3100 npm run test:e2e

Deployment

A. Cloudflare Pages (primary demo target)

Connect the GitHub repository and select production branch main:

  • Project name: linuxabi-eesti (if available)
  • Framework: Next.js (Static HTML Export)
  • Build command: npm run build
  • Output directory: out
  • Build environment: NODE_VERSION=22, NEXT_PUBLIC_SITE_URL=https://linuxabi-eesti.pages.dev

Use the assigned Pages URL if the requested name is unavailable. Public values must be set before building. Pushes to main then publish automatically.

No server runtime is required. Pages serves the static files in out/ (including the .txt RSC payloads the App Router uses for client-side navigation); the build is a plain folder upload.

Mock detail routes use generateStaticParams with dynamicParams = false. Query-string state (directory and marketplace filters, /abi/soov preselection) is resolved in the browser with useSearchParams, so deep links such as /abi?asukoht=Tartu keep working. Visitor-created questions use /kusimused/kohalik?slug=… and the existing localStorage viewer, so new questions also survive page refresh on a static host.

B. Local static preview (optional)

npm run start is a convenience helper that serves the exported out/ directory locally. It is not used by Cloudflare Pages and no Node process is needed in production.

npm ci
npm run build
PORT=3000 npm run start            # http://localhost:3000

Because the output is a plain folder of static files, any static host or web server (nginx, Caddy, Apache, object storage) can serve out/ directly.

C. Docker

docker build -t linuxabi-eesti:demo .
docker run --rm -p 3000:3000 linuxabi-eesti:demo

or:

docker compose up --build

NEXT_PUBLIC_* values are inlined at build time, so pass them as build args:

docker build \
  --build-arg NEXT_PUBLIC_SITE_URL=https://example.ee \
  -t linuxabi-eesti:demo .

The image is a multi-stage Alpine build that runs as a non-root user and only serves the exported out/ directory.


Project structure

src/
  app/                     App Router routes (see docs/ARCHITECTURE.md)
  components/
    brand/                 Wordmark / logo mark
    forms/                 Service request, laptop inquiry, question, provider signup
    home/                  Homepage search bar
    laptops/               Marketplace cards, filters, gallery, marketplace view
    layout/                Header, mobile nav, footer, demo banner, page shell
    providers/             Provider cards/filters/directory/map/reviews
    questions/             Question list, thread, answer form
    ui/                    Small reusable primitives (Button, Badge, Rating, …)
  data/                    Authored mock datasets (providers, laptops, reviews, …)
  lib/
    repositories/          Data-access contracts + mock implementations
    filters.ts             Provider/laptop filtering, sorting, scoring
    geo.ts                 Haversine distance helpers
    query.ts               URL <-> filter mapping (deep-linkable searches)
    storage.ts             Safe, namespaced localStorage helpers
    demo-store.ts          External store for visitor-created demo content
    types.ts               Domain model
docs/
  ARCHITECTURE.md          Design decisions, boundaries, data flow
  PRODUCTION.md            What must change to turn the demo into a real service
e2e/                       Playwright end-to-end flows
public/laptops/            Generated local laptop illustrations (SVG)
scripts/                   Laptop art generator, static preview helper

Demo data

All datasets live under src/data/ and are typed by src/lib/types.ts:

  • 20 providers covering all 15 Estonian counties (src/data/providers.ts). Real city coordinates, completely invented identities. Provider ratings are never stored on the provider record — they are derived from the review dataset in src/lib/repositories/mock.ts, so a card, a profile and the review list can never disagree.
  • 60 reviews with varied ratings (roughly 3.4–5.0), titles, bodies, service types, dates and occasional provider responses (src/data/reviews.ts).
  • 20 laptops — ThinkPad, Latitude, EliteBook, ProBook and similar business-class machines, mostly with Linux Mint 22.3 (Cinnamon or XFCE), one dual-boot unit (src/data/laptops.ts). Prices, battery health, cosmetic grades and warranty periods vary deliberately.
  • 20 questions / 30 answers across 10 categories, three intentionally unanswered so the "vastamata" filter is meaningful (src/data/questions.ts).
  • Locations — Estonian cities and counties with approximate coordinates (src/data/locations.ts).

Names, phone numbers and emails are avoided on purpose: profiles use obviously fictional contact placeholders and push visitors toward the request form instead of publishing details that could accidentally belong to a real person.

Product imagery is not downloaded from the internet. npm run art:laptops generates stable, repository-local SVG illustrations (three views per laptop) so the marketplace looks believable without external dependencies or licensing risk.


Map configuration

Map settings are isolated in src/lib/map-config.ts so the tile provider can be changed without touching a single map component:

MAP_TILE_URL      // NEXT_PUBLIC_MAP_TILE_URL, defaults to tile.openstreetmap.org
MAP_TILE_ATTRIBUTION
MAP_MAX_ZOOM
MAP_SELECTED_ZOOM

Leaflet is imported only through a dynamically loaded, client-only component (ProviderMap → ProviderMapInner with ssr: false), so it never ships to routes that do not render a map.

The demo uses the public OpenStreetMap tile server. That is fine for a low-traffic demonstration with visible attribution, but a real commercial or high-traffic deployment must select its own tile provider or self-hosted tiles according to that provider's usage policy (no bulk downloading, no offline prefetching, respect normal browser caching). No form contents or personal data are ever sent to map services.


localStorage behaviour

Only interactive demo state is persisted, always under a namespaced prefix (linuxabi-eesti.v1.):

Key Contents
…saved-providers / …saved-laptops Visitor's saved (favourite) items
…reviews Demo reviews the visitor added
…questions / …answers Demo questions and answers the visitor added
…service-requests The last demo service request (not transmitted)
…notice-dismissed Whether the global demo notice was hidden

Implementation notes:

  • All access goes through src/lib/storage.ts, which probes availability and degrades to safe no-ops during server rendering, in private browsing or when storage is full.
  • Visitor-created content is read through an external store (src/lib/demo-store.ts) consumed with useSyncExternalStore. Server render and first client render see the same empty state, so hydration never mismatches.
  • User text is rendered as normal React text nodes — never with dangerouslySetInnerHTML.
  • "Lähtesta demoandmed" (in the footer) removes exactly the keys above and nothing else on the origin.

Replacing the mock data layer with a real backend

Pages never import the datasets directly; they go through repository interfaces in src/lib/repositories/types.ts, resolved in src/lib/repositories/index.ts.

To introduce a real backend:

  1. Implement the same interfaces (ProviderRepository, LaptopRepository, ReviewRepository, QuestionRepository) against your API, PostgreSQL or Supabase client.
  2. Change the single wiring point in src/lib/repositories/index.ts.
  3. Keep the functions returning the domain types from src/lib/types.ts.

No UI component needs to change. docs/PRODUCTION.md lists the logical entities, the fields that move to the server, and the recommended migration order.


Productionization recommendations

  • Keep the demo notice unless the dataset is replaced with verified real data.
  • Replace the tile provider before real traffic (see above).
  • Add a real request/review/question pipeline, moderation and notifications.
  • Get Estonian/EU marketplace, consumer-protection, privacy and commercial terms reviewed before operating commercially.
  • Reconsider robots/noindex metadata before the content becomes real (fictional provider and laptop pages are currently noindex on purpose).
  • Review security headers and consider a CSP once third-party services are added.

Independence disclaimers

LinuxAbi Eesti is an independent demonstration project. It is not affiliated with, sponsored by or endorsed by:

  • Linux Mint or the Linux Mint project. The visual design is only loosely inspired by a cheerful green palette; the official Linux Mint logo and website are not reproduced. The official site is linked for reference: https://linuxmint.com
  • Qortal. Qortal is mentioned only as a neutral secondary reference to decentralized publishing technology, and the official project site is linked at https://qortal.org. This is not a cryptocurrency promotion and the demo is not affiliated with Qortal.

Used by

Contributors

Languages