diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..53a925b --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,28 @@ +# [PR Title] + +## Overview + +Brief description of what this PR does and why. +Mention any context, background, or goals. + +## Changes + +- High-level summary of key changes +- List of components, features, or files added/modified +- Reference design tokens, breakpoints, or accessibility improvements if relevant + +## Screenshots + + + +## How to Test + +1. Steps to run locally or in Storybook +2. List what to verify (e.g., responsive layout, accessibility, functional links) +3. Include breakpoints, browsers, or devices if relevant + +## Notes + +- Any future follow-ups, TODOs, or known limitations +- Temporary setups or workarounds (e.g., personal deployments, placeholder assets) +- External dependencies (e.g., design tokens, API connections) diff --git a/.gitignore b/.gitignore index a46e550..9cf2b15 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ next-env.d.ts *storybook.log storybook-static + +# storybook config files (to avoid git changes when switching between local and production) +.storybook/main.js +.storybook/preview.js diff --git a/.storybook/main.github.js b/.storybook/main.github.js new file mode 100644 index 0000000..7afbb5d --- /dev/null +++ b/.storybook/main.github.js @@ -0,0 +1,35 @@ +/** @type { import('@storybook/nextjs-vite').StorybookConfig } */ +const config = { + stories: [ + "../stories/**/*.mdx", + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", + ], + addons: [ + "@chromatic-com/storybook", + "@storybook/addon-docs", + "@storybook/addon-onboarding", + "@storybook/addon-a11y", + "@storybook/addon-vitest", + ], + framework: { + name: "@storybook/nextjs-vite", + options: {}, + }, + staticDirs: ["../public"], + managerHead: (head) => `${head}`, + previewHead: (head) => `${head}`, + async viteFinal(cfg) { + // IMPORTANT: Set base path for GitHub Pages sub-path hosting + cfg.base = "/communityrulestorybook/"; + // Ensure esbuild treats .js as JSX during dep pre-bundling + cfg.optimizeDeps ??= {}; + cfg.optimizeDeps.esbuildOptions ??= {}; + cfg.optimizeDeps.esbuildOptions.loader = { + ...(cfg.optimizeDeps.esbuildOptions.loader || {}), + ".js": "jsx", + ".ts": "tsx", + }; + return cfg; + }, +}; +export default config; diff --git a/.storybook/main.js b/.storybook/main.js index c3d3eb1..7afbb5d 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -21,7 +21,6 @@ const config = { async viteFinal(cfg) { // IMPORTANT: Set base path for GitHub Pages sub-path hosting cfg.base = "/communityrulestorybook/"; - // Ensure esbuild treats .js as JSX during dep pre-bundling cfg.optimizeDeps ??= {}; cfg.optimizeDeps.esbuildOptions ??= {}; diff --git a/.storybook/main.local.js b/.storybook/main.local.js new file mode 100644 index 0000000..6e3993d --- /dev/null +++ b/.storybook/main.local.js @@ -0,0 +1,31 @@ +/** @type { import('@storybook/nextjs-vite').StorybookConfig } */ +const config = { + stories: [ + "../stories/**/*.mdx", + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", + ], + addons: [ + "@chromatic-com/storybook", + "@storybook/addon-docs", + "@storybook/addon-onboarding", + "@storybook/addon-a11y", + "@storybook/addon-vitest", + ], + framework: { + name: "@storybook/nextjs-vite", + options: {}, + }, + staticDirs: ["../public"], + async viteFinal(cfg) { + // Ensure esbuild treats .js as JSX during dep pre-bundling + cfg.optimizeDeps ??= {}; + cfg.optimizeDeps.esbuildOptions ??= {}; + cfg.optimizeDeps.esbuildOptions.loader = { + ...(cfg.optimizeDeps.esbuildOptions.loader || {}), + ".js": "jsx", + ".ts": "tsx", + }; + return cfg; + }, +}; +export default config; diff --git a/.storybook/preview.github.js b/.storybook/preview.github.js new file mode 100644 index 0000000..1fd25df --- /dev/null +++ b/.storybook/preview.github.js @@ -0,0 +1,16 @@ +import "../app/globals.css"; + +/** @type { import('@storybook/react').Preview } */ +const preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/.storybook/preview.js b/.storybook/preview.js index bd769ce..1fd25df 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,62 +1,15 @@ import "../app/globals.css"; -/** @type { import('@storybook/nextjs-vite').Preview } */ +/** @type { import('@storybook/react').Preview } */ const preview = { parameters: { - nextjs: { appDirectory: true }, + actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, - - a11y: { - // 'todo' - show a11y violations in the test UI only - // 'error' - fail CI on a11y violations - // 'off' - skip a11y checks entirely - test: "todo", - }, - - backgrounds: { - default: "dark", - values: [ - { - name: "dark", - value: "#000000", - }, - { - name: "light", - value: "#ffffff", - }, - ], - }, - - viewport: { - defaultViewport: "md", - viewports: { - xsm: { - name: "XSmall (≤429px)", - styles: { width: "429px", height: "800px" }, - }, - sm: { - name: "Small (≥430px)", - styles: { width: "430px", height: "800px" }, - }, - md: { - name: "Medium (≥640px)", - styles: { width: "640px", height: "800px" }, - }, - lg: { - name: "Large (≥1024px)", - styles: { width: "1024px", height: "800px" }, - }, - xl: { - name: "XLarge (≥1440px)", - styles: { width: "1440px", height: "900px" }, - }, - }, - }, }, }; diff --git a/.storybook/preview.local.js b/.storybook/preview.local.js new file mode 100644 index 0000000..1fd25df --- /dev/null +++ b/.storybook/preview.local.js @@ -0,0 +1,16 @@ +import "../app/globals.css"; + +/** @type { import('@storybook/react').Preview } */ +const preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/README.md b/README.md index 66bb426..b70ece1 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,79 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# Community Rule + +A Next.js application for community decision-making and governance documentation. ## Getting Started -First, run the development server: +Run the development server: ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. +## Storybook Development -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +This project includes Storybook for component development and documentation. The setup supports both local development and GitHub Pages deployment. -## Learn More +### Local Development -To learn more about Next.js, take a look at the following resources: +For local Storybook development (no base path): -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +```bash +npm run storybook:local +``` -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +This will: -## Deploy on Vercel +- Copy local configuration files (without GitHub Pages base path) +- Start Storybook at `http://localhost:6006` +- Ignore configuration changes in git -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +### Production Deployment -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +When ready to deploy to GitHub Pages: + +1. **Restore GitHub Pages configuration:** + + ```bash + npm run storybook:restore + ``` + +2. **Build Storybook:** + + ```bash + npm run build-storybook + ``` + +3. **Deploy to GitHub Pages repository:** + + ```bash + # Copy the build to your GitHub Pages repository + cp -r storybook-static/* /path/to/communityrulestorybook/ + + # Or if you have it as a git submodule: + cp -r storybook-static/* communityrulestorybook/ + cd communityrulestorybook + git add . + git commit -m "Update Storybook build" + git push origin main + ``` + +### Switching Between Configurations + +- **Local Development:** `npm run storybook:local` +- **Production Build:** `npm run storybook:restore` then `npm run build-storybook` +- **Back to Local:** `npm run storybook:local` + +The gitignore is configured to prevent configuration file changes from triggering git changes during local development. + +### Available Scripts + +- `npm run dev` - Start Next.js development server +- `npm run build` - Build Next.js application for production +- `npm run start` - Start Next.js production server +- `npm run storybook:local` - Start Storybook with local configuration +- `npm run storybook:restore` - Restore GitHub Pages configuration +- `npm run build-storybook` - Build Storybook for production +- `npm run storybook` - Start Storybook with current configuration diff --git a/app/components/LogoWall.js b/app/components/LogoWall.js new file mode 100644 index 0000000..d3a8c5d --- /dev/null +++ b/app/components/LogoWall.js @@ -0,0 +1,103 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Image from "next/image"; + +const LogoWall = ({ logos = [] }) => { + const [isVisible, setIsVisible] = useState(false); + + // Default logos if none provided - ordered for mobile (3 rows × 2 columns) + const defaultLogos = [ + { + src: "assets/Section/Logo_FoodNotBombs.png", + alt: "Food Not Bombs", + size: "h-11 lg:h-14 xl:h-[70px]", + order: "order-1 sm:order-4", // Mobile: row 1 col 1, SM: row 2 col 1 (bottom left) + }, + { + src: "assets/Section/Logo_StartCOOP.png", + alt: "Start COOP", + size: "h-[42px] lg:h-[53px] xl:h-[66px]", + order: "order-2 sm:order-2", // Mobile: row 1 col 2, SM: row 1 col 2 (top middle) + }, + { + src: "assets/Section/Logo_Metagov.png", + alt: "Metagov", + size: "h-6 lg:h-8 xl:h-[41px]", + order: "order-3 sm:order-1", // Mobile: row 2 col 1, SM: row 1 col 1 (top left) + }, + { + src: "assets/Section/Logo_OpenCivics.png", + alt: "Open Civics", + size: "h-8 lg:h-10 xl:h-[50px]", + order: "order-4 sm:order-5 md:order-6", // Mobile: row 2 col 2, SM: row 2 col 2, MD: swapped with Mutual Aid CO + }, + { + src: "assets/Section/Logo_MutualAidCO.png", + alt: "Mutual Aid CO", + size: "h-11 lg:h-14 xl:h-[70px]", + order: "order-5 sm:order-6 md:order-5", // Mobile: row 3 col 1, SM: row 2 col 3, MD: swapped with OpenCivics + }, + { + src: "assets/Section/Logo_CUBoulder.png", + alt: "CU Boulder", + size: "h-10 lg:h-12 xl:h-[60px]", + order: "order-6 sm:order-3", // Mobile: row 3 col 2, SM: row 1 col 3 (top right) + }, + ]; + + const displayLogos = logos.length > 0 ? logos : defaultLogos; + + // Simple fade-in effect after component mounts + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, 100); + + return () => clearTimeout(timer); + }, []); + + return ( +
+
+ {/* Label */} +

+ Trusted by leading cooperators +

+ + {/* Logo Grid Container */} +
+
+ {displayLogos.map((logo, index) => ( +
+ {logo.alt} +
+ ))} +
+
+
+
+ ); +}; + +export default LogoWall; diff --git a/app/layout.js b/app/layout.js index e36ac0e..18e97dc 100644 --- a/app/layout.js +++ b/app/layout.js @@ -11,7 +11,7 @@ const inter = Inter({ const bricolageGrotesque = Bricolage_Grotesque({ subsets: ["latin"], - weight: ["400"], + weight: ["400", "500"], variable: "--font-bricolage-grotesque", }); diff --git a/app/page.js b/app/page.js index 78c8ecb..5e07816 100644 --- a/app/page.js +++ b/app/page.js @@ -1,5 +1,6 @@ import NumberedCards from "./components/NumberedCards"; import HeroBanner from "./components/HeroBanner"; +import LogoWall from "./components/LogoWall"; export default function Page() { const heroBannerData = { @@ -36,6 +37,7 @@ export default function Page() { return (
+
); diff --git a/package.json b/package.json index 233af71..76a0703 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint", "postinstall": "npm rebuild lightningcss", "storybook": "storybook dev -p 6006", + "storybook:local": "cp .storybook/main.local.js .storybook/main.js && cp .storybook/preview.local.js .storybook/preview.js && storybook dev -p 6006", + "storybook:restore": "cp .storybook/main.github.js .storybook/main.js && cp .storybook/preview.github.js .storybook/preview.js", "build-storybook": "storybook build" }, "dependencies": { @@ -31,7 +33,7 @@ "@vitest/browser": "^3.2.4", "@vitest/coverage-v8": "^3.2.4", "eslint": "^9", - "eslint-config-next": "15.2.4", + "eslint-config-next": "15.2.0", "eslint-plugin-storybook": "^9.1.2", "playwright": "^1.54.2", "postcss": "^8.5.6", diff --git a/public/assets/Section/Logo_CUBoulder.png b/public/assets/Section/Logo_CUBoulder.png new file mode 100644 index 0000000..7802cbf Binary files /dev/null and b/public/assets/Section/Logo_CUBoulder.png differ diff --git a/public/assets/Section/Logo_FoodNotBombs.png b/public/assets/Section/Logo_FoodNotBombs.png new file mode 100644 index 0000000..b7e1369 Binary files /dev/null and b/public/assets/Section/Logo_FoodNotBombs.png differ diff --git a/public/assets/Section/Logo_Metagov.png b/public/assets/Section/Logo_Metagov.png new file mode 100644 index 0000000..f1e0816 Binary files /dev/null and b/public/assets/Section/Logo_Metagov.png differ diff --git a/public/assets/Section/Logo_MutualAidCO.png b/public/assets/Section/Logo_MutualAidCO.png new file mode 100644 index 0000000..43ec89e Binary files /dev/null and b/public/assets/Section/Logo_MutualAidCO.png differ diff --git a/public/assets/Section/Logo_OpenCivics.png b/public/assets/Section/Logo_OpenCivics.png new file mode 100644 index 0000000..7ac21a2 Binary files /dev/null and b/public/assets/Section/Logo_OpenCivics.png differ diff --git a/public/assets/Section/Logo_StartCOOP.png b/public/assets/Section/Logo_StartCOOP.png new file mode 100644 index 0000000..9e67a1a Binary files /dev/null and b/public/assets/Section/Logo_StartCOOP.png differ diff --git a/stories/LogoWall.stories.js b/stories/LogoWall.stories.js new file mode 100644 index 0000000..4e58e6f --- /dev/null +++ b/stories/LogoWall.stories.js @@ -0,0 +1,83 @@ +import LogoWall from "../app/components/LogoWall"; + +export default { + title: "Components/LogoWall", + component: LogoWall, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: `A responsive logo wall component that displays partner/sponsor logos in a grid layout. Features responsive breakpoints with different layouts and sizing for mobile, tablet, and desktop views. + +## Responsive Behavior + +- **Mobile**: 3 rows × 2 columns grid with 32px gaps +- **SM**: 2 rows × 3 columns grid with 48px row gap and 32px column gap +- **MD**: Single row with space-between layout and 24px gap between text and logos +- **LG**: Larger logo sizes and 64px horizontal padding +- **XL**: Largest logo sizes, 160px horizontal padding, and 14px label text + +## Animations & Transitions + +- **Fade-in Effect**: Logos fade in from opacity 0 to 60% after component mounts (500ms transition) +- **Hover Interactions**: Individual logos respond to hover with: + - Opacity change from 60% to 100% + - Scale transform (105% zoom) + - 500ms smooth transitions for all effects +- **Loading States**: Progressive loading with fallback timer for reliable display + +## Props + +- **logos** (optional): Array of logo objects with src, alt, size, and order properties. If not provided, uses default partner logos. + +## Usage Examples + +### Custom Logos +\`\`\`jsx + +\`\`\` + +### Empty State +\`\`\`jsx + +\`\`\` +This will fall back to the default partner logos.`, + }, + }, + }, + tags: ["autodocs"], + argTypes: { + logos: { + control: "object", + description: + "Array of logo objects with src, alt, size, and order properties. If not provided, uses default partner logos.", + }, + }, +}; + +export const Default = { + args: {}, + parameters: { + docs: { + description: { + story: + "Default LogoWall with all partner logos. Displays in a 3×2 grid on mobile, 2×3 grid on small screens, single row on medium screens, and larger sizes on large screens. Features smooth fade-in animations and hover interactions.", + }, + }, + }, +};