Chat about this codebase

AI-powered code exploration

Online

Project Overview

This section introduces Impact Blog, outlines its core features, and summarizes the tech stack. Frontend developers and contributors will learn what the project does and why it matters.

What is Impact Blog?

Impact Blog is a content-driven platform that connects mission-focused organizations with supporters. It combines editorial content, an online shop, and donation flows for an integrated giving experience.

Main Features

  • Campaign Articles
    Publish and browse long-form articles to showcase impact stories.
  • Magazine Shop
    Sell print and digital editions using a Stripe-powered checkout.
  • Supporter Flows
    Guide users through one-time and recurring donations, complete with confirmation and receipts.

Why It Matters

Impact Blog centralizes storytelling and fundraising in a single React application. It empowers nonprofit organizations to engage audiences, drive donations, and manage content without maintaining multiple platforms.

Technology Stack

  • Frontend
    • React 18 + TypeScript
    • Vite for fast dev server and builds
    • Tailwind CSS for utility-first styling
  • API Layer
    • Axios for HTTP requests to the backend
  • Payments
    • Stripe Checkout and Elements for secure transactions
  • Tooling
    • ESLint with TypeScript-aware rules and React plugins
    • Prettier for code formatting

Example: Key NPM Scripts

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

Who Should Read This

  • Frontend developers joining the Impact Blog codebase
  • Contributors adding features, fixing bugs, or improving styling
  • UX engineers integrating new payment flows or customizing layouts

Getting Started

Follow these steps to clone, install, configure, and run the Impact Blog locally or in preview mode. You’ll also learn how to lint, format, and type-check the code. Vite’s React plugin provides Fast Refresh; TailwindCSS uses JIT for instant style updates.

1. Prerequisites

• Node.js ≥ 14.x
• npm ≥ 6.x

2. Clone & Install

git clone https://github.com/just-lend/impact-blog.git
cd impact-blog
npm install

3. Configure Environment Variables

Create a .env.local file at project root. Vite exposes variables prefixed with VITE_.

Example .env.local:

VITE_API_URL=https://api.example.com
VITE_ANALYTICS_ID=UA-XXXXXXX-X

Restart the dev server after changing .env.local.

4. Development

Run the local dev server with Fast Refresh and Tailwind JIT:

npm run dev

• Access at http://localhost:5173
• Tailwind rebuilds styles on file save
• React components hot-reload with state preservation

5. Preview Mode

Build and serve a production-like preview:

npm run build
npm run preview

• Serves optimized build on http://localhost:4173
• Matches production asset handling

6. Production Start

The Procfile defines the web process for platforms like Heroku:

web: npm start

npm start invokes Vite’s preview server (equivalent to npm run preview).

7. Linting & Formatting

• Run ESLint checks:

npm run lint

• Auto-fix and format with Prettier:

npm run format

ESLint config extends React and TypeScript rules for type-aware linting.

8. Type Checking

Project-wide type checking uses referenced configs in tsconfig.json:

npm run type-check

This runs tsc --build, validating both app and Node.js environment settings.

9. Configuration Reference

tsconfig.json

{
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}

Update tsconfig.app.json and tsconfig.node.json to adjust environment-specific compiler options.

vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from 'tailwindcss';

export default defineConfig({
  plugins: [
    react(),      // Enables Fast Refresh
    tailwindcss() // JIT styling
  ],
  server: {
    port: 5173
  }
});

Customize Vite settings here for proxying APIs, aliasing, or adjusting build options.

Application Architecture

This section outlines the high-level structure of the React application in the just-lend/impact-blog repository. You’ll see how routing, shared utilities, and styling configuration tie together to provide a consistent developer experience.


Routing Configuration in App.tsx

Client-side routing uses react-router-dom with scroll restoration and a consistent layout wrapper.

Imports and Setup

// src/App.tsx
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ScrollToTop from "./components/ScrollToTop";
import Header from "./components/Header";
import Footer from "./components/Footer";
import LayoutContainer from "./components/LayoutContainer";

// Page components
import Magazine from "./pages/Magazine";
import ArticlePage from "./pages/Article";
import CategorisedArticles from "./pages/CategorisedArticles";
import About from "./pages/About";
import MerchProductView from "./pages/MerchProductView";

Defining Routes

const App: React.FC = () => (
  <Router>
    <ScrollToTop />     {/* Resets scroll on every navigation */}
    <Header />          {/* Persistent top navigation */}
    <LayoutContainer>  
      <Routes>
        <Route path="/" element={<Magazine />} />
        <Route path="/articles/:slug" element={<ArticlePage />} />
        <Route path="/categories/:id" element={<CategorisedArticles />} />
        <Route path="/about" element={<About />} />
        <Route path="/merch/:slug" element={<MerchProductView />} />
        {/* Add more routes here */}
      </Routes>
    </LayoutContainer>
    <Footer />          {/* Persistent footer */}
  </Router>
);

export default App;

Key Points

  • ScrollToTop: Uses useLocation + window.scrollTo to scroll to top on route change.
  • LayoutContainer: Applies global max-width, padding and centers content.
  • Persistent UI: <Header /> and <Footer /> live outside <Routes>.
  • Dynamic Segments: URL params (:slug, :id) are accessed via useParams() in page components.

Practical Usage

  1. Add a New Page

    • Create src/pages/Contact.tsx, use useParams() if needed.
    • Import in App.tsx and add
      <Route path="/contact" element={<Contact />} />
      
  2. Nested Routes / Layouts

    <Route path="/admin" element={<AdminLayout />}>
      <Route index element={<AdminDashboard />} />
      <Route path="users" element={<UserList />} />
      <Route path="users/:id" element={<UserDetail />} />
    </Route>
    
  3. AppContainer Wrapper

    // src/AppContainer.tsx
    function AppContainer() {
      return (
        <div className="min-h-screen w-full flex flex-col">
          <App />
        </div>
      );
    }
    export default AppContainer;
    

Alerts Utility Functions

A thin wrapper around SweetAlert2 for consistent, promise-based dialogs.

Import

// src/components/alerts/index.ts
import Swal, { SweetAlertResult } from "sweetalert2";

export function showLoadingAlert(message?: string): Promise<SweetAlertResult> { /*...*/ }
export function showSuccessAlert(message: string): Promise<SweetAlertResult> { /*...*/ }
export function showErrorAlert(message: string): Promise<SweetAlertResult> { /*...*/ }
export function showInfoAlert(message: string): Promise<SweetAlertResult> { /*...*/ }
export function showConfirmDialogAlert(
  heading: string,
  description: string
): Promise<SweetAlertResult> { /*...*/ }

Functions Overview

  • showLoadingAlert(message?: string): Spinner + message (default “Please wait…”).
  • showSuccessAlert(message: string): Check-icon success dialog.
  • showErrorAlert(message: string): Cross-icon error dialog.
  • showInfoAlert(message: string): Info-icon dialog.
  • showConfirmDialogAlert(heading, description): “Yes/No” warning; resolves with .isConfirmed.

Code Examples

import {
  showLoadingAlert,
  showSuccessAlert,
  showErrorAlert,
  showConfirmDialogAlert
} from "src/components/alerts";

async function submitForm(data: FormData) {
  showLoadingAlert("Submitting…");
  try {
    await api.submit(data);
    Swal.close(); // close loading
    await showSuccessAlert("Submission successful!");
  } catch {
    Swal.close();
    await showErrorAlert("Submission failed. Try again.");
  }
}

async function handleDelete(id: string) {
  const result = await showConfirmDialogAlert(
    "Delete this item?",
    "This action cannot be undone."
  );
  if (result.isConfirmed) {
    await api.deleteItem(id);
    await showSuccessAlert("Item deleted.");
  }
}

Tips

  • All functions return a Promiseawait to sequence dialogs.
  • Call Swal.close() to dismiss a loading alert before showing another.
  • To customize defaults, configure Swal mixins at app entry:
    import Swal from "sweetalert2";
    Swal.mixin({ customClass: { confirmButton: "btn btn-primary" } });
    

Tailwind Configuration: Custom Screens & Font Family

Extend Tailwind’s breakpoints and fonts to match your design system.

tailwind.config.js

// tailwind.config.js
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    screens: {
      monitor: { max: "2560px" },
      desktop: { max: "1279px" },
      tablet:  { max: "1199px" },
      mobile:  { max: "820px" }
    },
    extend: {
      fontFamily: {
        hanken: ["Hanken Grotesk", "sans-serif"]
      }
    }
  },
  plugins: []
};

Usage in JSX

<div className="p-8 monitor:p-6 desktop:p-4 mobile:p-2">
  Responsive padding
</div>

<h1 className="font-hanken text-3xl mobile:text-2xl">
  Welcome to Our Site
</h1>

Global Styles (src/index.css)

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  font-family: "Hanken Grotesk", sans-serif; /* syncs with tailwind.config.js */
  max-width: 1820px;
  margin: 0 auto;
  display: grid;
  place-items: center;
}

.article-page img {
  width: 60%;
  margin: 2rem auto;
}
.article-page .coverImage {
  object-fit: cover;
  width: 100%;
  max-height: 40vh;
}
.article-page p {
  margin: 2rem 0;
}

Practical Tips

  • Use custom screen names (monitor, desktop, tablet, mobile) instead of default sm/md/lg.
  • Prefix utilities with breakpoints: desktop:text-lg, tablet:grid-cols-2.
  • Apply font-hanken for all headings and body text to maintain consistency.

Data & API Integration

This section describes how the React front-end communicates with backend services via Axios, how to configure endpoints, and where to update them.

1. Configuring the API Base URL

All service modules use axios.defaults.baseURL pointing to import.meta.env.VITE_API_URL.

  1. Create a file .env.local at project root:
    VITE_API_URL=https://api.your-domain.com
    
  2. Ensure Vite loads it by restarting the dev server.
  3. In your service entry (e.g. src/api/index.ts), set:
    import axios from "axios";
    axios.defaults.baseURL = import.meta.env.VITE_API_URL;
    
  4. To change endpoints for different environments, override VITE_API_URL in .env.production or CI variables.

2. CampaignArticlesService

Client-side wrappers around /campaignArticles endpoints. All methods return res.data and may throw on HTTP errors.

// src/api/campaignArticleService.ts
import axios from "axios";

// Fetch latest articles (limit defaults to 10)
export async function getCampaignArticles(limit = 10) {
  const res = await axios.get(`/campaignArticles/latest-campaign-articles`, {
    params: { limit },
  });
  return res.data; // array of CampaignArticle
}

// Fetch a single article by slug
export async function getCampaignArticleBySlug(slug: string) {
  const res = await axios.get(`/campaignArticles/${slug}`);
  return res.data; // CampaignArticle
}

// Fetch articles by category filter
export async function getCampaignArticlesByCategory(
  categoryId: number,
  categoryName: string
) {
  const res = await axios.get(`/campaignArticles/filter-by-category`, {
    params: { categoryId, categoryName },
  });
  return res.data;
}

// Fetch by UNSDG number
export async function getCampaignArticlesByUNSDG(unsdg: number) {
  const res = await axios.get(`/campaignArticles/filter-by-unsdg`, {
    params: { unsdg },
  });
  return res.data;
}

// Fetch by local authority ID
export async function getCampaignArticlesByLocalAuthorityId(
  localAuthorityId: number
) {
  const res = await axios.get(`/campaignArticles/filter-by-local-authority`, {
    params: { localAuthorityId },
  });
  return res.data;
}

Usage in a Component

import { useEffect, useState } from "react";
import {
  getCampaignArticles,
  getCampaignArticleBySlug,
} from "@/api/campaignArticleService";
import type { CampaignArticle } from "@/types";

export default function ArticleList() {
  const [articles, setArticles] = useState<CampaignArticle[]>([]);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    getCampaignArticles()
      .then(setArticles)
      .catch(err => setError(err.response?.data?.message || err.message));
  }, []);

  if (error) return <p>Error: {error}</p>;
  return (
    <ul>
      {articles.map(a => (
        <li key={a.id}>
          <a href={`/articles/${a.slug}`}>{a.title}</a>
        </li>
      ))}
    </ul>
  );
}

3. CampaignCategoriesService

Retrieve campaign categories for filtering and navigation.

// src/api/campaignCategories.ts
import axios from "axios";

export async function getAllCampaignCategories() {
  const res = await axios.get("/campaignCategories");
  return res.data; // Category[]
}

Dropdown Example

import { useEffect, useState } from "react";
import { getAllCampaignCategories } from "@/api/campaignCategories";
import type { Category } from "@/types";

export function CategorySelector() {
  const [categories, setCategories] = useState<Category[]>([]);

  useEffect(() => {
    getAllCampaignCategories().then(setCategories);
  }, []);

  return (
    <select>
      {categories.map(c => (
        <option key={c.id} value={c.id}>{c.name}</option>
      ))}
    </select>
  );
}

4. StripeService

Helper functions for Stripe customer and checkout session endpoints.

// src/api/stripeService.ts
import axios from "axios";

export async function createStripeCustomer(email: string, name: string) {
  const res = await axios.post("/stripe/customer", { email, name });
  return res.data; // { customer: { id, email, ... } }
}

export async function getStripeCustomer(email: string) {
  const res = await axios.get(`/stripe/customer?email=${email}`);
  return res.data; // { customer: object|null }
}

export async function oneTimeCheckout(data: {
  url: string;
  prodName: string;
  amount: number;
  email: string;
}) {
  const res = await axios.post("/stripe/checkout/one-time", data);
  return res.data; // { id: string; url: string; }
}

export async function monthlyCheckout(data: {
  url: string;
  prodName: string;
  amount: number;
  email: string;
}) {
  const res = await axios.post("/stripe/checkout/subscription", data);
  return res.data;
}

Redirect Example

import { oneTimeCheckout } from "@/api/stripeService";

async function donate() {
  const session = await oneTimeCheckout({
    url: "https://app.com/thanks",
    prodName: "Impact Donation",
    amount: 500,
    email: "user@example.com",
  });
  window.location.href = session.url;
}

5. Type Definitions

Import all interfaces from src/types to enforce type safety in components and services.

CampaignArticle (excerpt)

export interface CampaignArticle {
  id: number;
  title: string;
  slug: string;
  preview: string;
  image_urls: string[];
  categories: string[];
  sdgs: number[];
  localAuthorityId: number;
}

Category

export interface Category {
  id: number;
  name: string;
}

StripeSession

export interface StripeSession {
  id: string;
  url: string;
}

Practical Tips

  • Wrap all API calls in try/catch to handle errors and inspect err.response.
  • For custom limits or filters, extend service methods with additional parameters.
  • To point at a mock server or alternate host, update VITE_API_URL without touching code.
  • Leverage strict TS settings and runtime validators (e.g. Zod) before casting API responses.

Payment & Donation Workflows

Provides an end-to-end walkthrough of the donation and subscription flows: UI components, Stripe service integration, checkout session creation, and referral tracking. Maintainers can safely update amounts, plans, and UI copy.

1. UI Pages

1.1 Support.tsx (ImpactSOS Zines)

Key features:

  • Supporter type radio buttons (monthly vs. one-time)
  • Amount selection (preset buttons + custom input)
  • Personal details: name, email
  • Local authority dropdown (fetched from API)
  • Referral code capture via URL query
// src/pages/Support.tsx
import { useRouter } from 'next/router'
import { useState, useEffect } from 'react'
import { supporterTypes } from '../components/constants/supporterTypes'
import * as stripeService from '../api/stripeService'

export default function Support() {
  const router = useRouter()
  const { ref: referral } = router.query as { ref?: string }

  const [supporterType, setSupporterType] = useState<string>(supporterTypes.OneTime)
  const [amount, setAmount] = useState<number>(10)
  const [name, setName] = useState<string>('')
  const [email, setEmail] = useState<string>('')
  const [localAuthorities, setLocalAuthorities] = useState<string[]>([])
  const [localAuthority, setLocalAuthority] = useState<string>('')

  // Fetch local authorities on mount
  useEffect(() => {
    fetch('/api/local-authorities')
      .then(res => res.json())
      .then((data: string[]) => setLocalAuthorities(data))
  }, [])

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    // 1. Create or retrieve Stripe customer
    const customer = await stripeService.createStripeCustomer({
      email,
      metadata: { name, localAuthority, referral: referral || '' }
    })

    // 2. Create Checkout Session
    const common = {
      customerId: customer.id,
      successUrl: `${window.location.origin}/thank-you`,
      cancelUrl: window.location.href,
      supporterType,
      referral: referral || ''
    }

    let sessionUrl: string
    if (supporterType === supporterTypes.OneTime) {
      const session = await stripeService.createOneTimeCheckoutSession({
        ...common,
        amount: amount * 100 // convert £ to pence
      })
      sessionUrl = session.url
    } else {
      const session = await stripeService.createSubscriptionCheckoutSession({
        ...common,
        priceId: process.env.NEXT_PUBLIC_MONTHLY_PRICE_ID!
      })
      sessionUrl = session.url
    }

    // 3. Redirect to Stripe Checkout
    router.push(sessionUrl)
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* Supporter Type */}
      <fieldset>
        <label>
          <input
            type="radio"
            name="type"
            value={supporterTypes.OneTime}
            checked={supporterType === supporterTypes.OneTime}
            onChange={() => setSupporterType(supporterTypes.OneTime)}
          /> One-time
        </label>
        <label>
          <input
            type="radio"
            name="type"
            value={supporterTypes.Monthly}
            checked={supporterType === supporterTypes.Monthly}
            onChange={() => setSupporterType(supporterTypes.Monthly)}
          /> Monthly
        </label>
      </fieldset>

      {/* Amount Selector (buttons or input) */}
      <div>
        {[5, 10, 20].map(val => (
          <button
            type="button"
            key={val}
            onClick={() => setAmount(val)}
            className={amount === val ? 'active' : ''}
          >
            £{val}
          </button>
        ))}
        <input
          type="number"
          min={1}
          value={amount}
          onChange={e => setAmount(Number(e.target.value))}
        />
      </div>

      {/* Personal Details */}
      <input
        type="text"
        placeholder="Full name"
        value={name}
        onChange={e => setName(e.target.value)}
        required
      />
      <input
        type="email"
        placeholder="Email address"
        value={email}
        onChange={e => setEmail(e.target.value)}
        required
      />

      {/* Local Authority */}
      <select
        value={localAuthority}
        onChange={e => setLocalAuthority(e.target.value)}
        required
      >
        <option value="">Select local authority</option>
        {localAuthorities.map(la => (
          <option key={la} value={la}>{la}</option>
        ))}
      </select>

      <button type="submit">Continue to Payment</button>
    </form>
  )
}

1.2 Support copy.tsx (Legacy Page)

Monthly slider with optional custom input over £10.

// src/pages/Support copy.tsx
import { useState } from 'react'
import Slider from '@material-ui/core/Slider'

export default function LegacySupport() {
  const [monthly, setMonthly] = useState<number>(10)
  const [custom, setCustom] = useState<string>('')

  const displayAmount = monthly > 10 && custom
    ? Number(custom)
    : monthly

  return (
    <div>
      <h2>Support us monthly</h2>
      <Slider
        value={monthly}
        min={1}
        max={50}
        step={1}
        onChange={(_, val) => setMonthly(val as number)}
      />
      {monthly > 10 && (
        <input
          type="number"
          placeholder="Custom amount"
          value={custom}
          onChange={e => setCustom(e.target.value)}
        />
      )}
      <p>You're pledging £{displayAmount} per month</p>
    </div>
  )
}

2. Stripe Service Integration

All Stripe calls live in src/api/stripeService.ts. Functions return Stripe-typed responses and session URLs.

// src/api/stripeService.ts
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2020-08-27' })

export interface SessionInput {
  customerId: string
  successUrl: string
  cancelUrl: string
  supporterType: string
  referral?: string
}

export async function createStripeCustomer(params: {
  email: string
  metadata?: Record<string, string>
}): Promise<Stripe.Customer> {
  return stripe.customers.create({ email: params.email, metadata: params.metadata })
}

export async function createOneTimeCheckoutSession(input: SessionInput & { amount: number }) {
  return stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    mode: 'payment',
    customer: input.customerId,
    line_items: [{ price_data: {
      currency: 'gbp',
      product_data: { name: 'One-Time Donation' },
      unit_amount: input.amount
    }, quantity: 1 }],
    metadata: { supporterType: input.supporterType, referral: input.referral || '' },
    success_url: input.successUrl,
    cancel_url: input.cancelUrl
  })
}

export async function createSubscriptionCheckoutSession(input: SessionInput & { priceId: string }) {
  return stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    mode: 'subscription',
    customer: input.customerId,
    line_items: [{ price: input.priceId, quantity: 1 }],
    metadata: { supporterType: input.supporterType, referral: input.referral || '' },
    success_url: input.successUrl,
    cancel_url: input.cancelUrl
  })
}

export async function retrieveStripeCustomer(customerId: string): Promise<Stripe.Customer> {
  return stripe.customers.retrieve(customerId) as Promise<Stripe.Customer>
}

3. Referral Tracking

  • Reads ref from router.query.
  • Stores in Stripe Customer metadata and Checkout Session metadata.
  • To change the parameter key, update the destructuring in Support.tsx and metadata mapping.

4. Updating Pricing & Plans

  • Monthly plan price IDs live in environment: NEXT_PUBLIC_MONTHLY_PRICE_ID.
  • To add tiers or change amounts:
    • Adjust preset buttons or slider min/max/step in the UI.
    • Update env vars and redeploy to reflect new Stripe price IDs.
  • Ensure supporterTypes constants (monthly, one-time) remain in sync with metadata logic.

Deployment & Environment Configuration

This section covers deploying just-lend/impact-blog to Heroku or any static host, configuring required environment variables, setting up CI type-checking, and optimizing images to keep the repo lean.

1. Environment Variables

Create a .env file at the project root. Vite exposes vars prefixed with VITE_ via import.meta.env.

VITE_API_BASE_URL=https://api.yourdomain.com
VITE_STRIPE_PUBLIC_KEY=pk_live_XXXXXXXXXXXXXXXXXXXX
# Only if you handle Stripe server-side (e.g. Netlify Functions):
STRIPE_SECRET_KEY=sk_live_XXXXXXXXXXXXXXXXXXXX

Access in code:

// src/api/client.ts
const API_BASE = import.meta.env.VITE_API_BASE_URL;
export async function fetchPosts() {
  const res = await fetch(`${API_BASE}/posts`);
  return res.json();
}

2. Heroku Deployment

  1. Add a Procfile:

    web: npm start
    
  2. Ensure package.json defines start and build scripts:

    // package.json (scripts section)
    {
      "scripts": {
        "dev": "vite",
        "build": "vite build",
        "preview": "vite preview",
        "start": "npm run preview",
        "type-check": "tsc --project tsconfig.app.json && tsc --project tsconfig.node.json"
      }
    }
    
  3. Commit and push to your Heroku remote:

    git add Procfile .env
    git commit -m "Configure Heroku deployment"
    git push heroku main
    heroku config:set VITE_API_BASE_URL=https://api.yourdomain.com \
                      VITE_STRIPE_PUBLIC_KEY=pk_live_... 
    

Heroku will run npm install, npm run build, then npm start (serving dist/).

3. Static Hosting

For Netlify, Vercel or any static host:

  1. Build the app:

    npm ci
    npm run build
    
  2. Deploy the dist/ folder.

  • Netlify: drag-and-drop dist/ or point a site at the repo and set npm run build as build command, dist as publish directory.
  • Vercel: automatically detects Vite; use npm run build (Output Directory: dist).

4. CI Type-Checking

Include type checks in CI to catch errors early.

Example GitHub Actions workflow .github/workflows/ci.yml:

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install dependencies
        run: npm ci
      - name: Type-check
        run: npm run type-check
      - name: Build
        run: npm run build

5. Image Optimization

Large images inflate repo size and build time. Use Vite plugins to compress during build:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwind from 'tailwindcss';
import viteImagemin from 'vite-plugin-imagemin';

export default defineConfig({
  plugins: [
    react(),
    viteImagemin({
      gifsicle: { optimizationLevel: 7 },
      optipng: { optimizationLevel: 7 },
      mozjpeg: { quality: 75 },
      svgo: { plugins: [{ removeViewBox: false }] }
    })
  ]
});

Alternatively:

  • Pre-compress assets with tools like ImageOptim before committing.
  • Serve images via a CDN (e.g., Cloudinary) and fetch on-the-fly.

This configuration streamlines deployment, enforces type safety, and keeps your build output optimized.