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) => (
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+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.",
+ },
+ },
+ },
+};