Initial Commit

This commit is contained in:
trouper
2025-08-10 13:15:38 -05:00
parent c0954329d1
commit 36dbfe5940
38 changed files with 74138 additions and 1327 deletions

11
next.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
images: {
domains: [],
},
}
module.exports = nextConfig

View File

@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"name": "defend-access", "name": "defendaccess-org",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -9,16 +9,24 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"react": "19.1.0", "file-saver": "^2.0.5",
"react-dom": "19.1.0", "framer-motion": "^11.0.0",
"next": "15.4.6" "jszip": "^3.10.1",
"lucide-react": "^0.263.1",
"next": "14.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@types/file-saver": "^2.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^18",
"@types/react-dom": "^19", "@types/react-dom": "^18",
"@tailwindcss/postcss": "^4", "autoprefixer": "^10.4.21",
"tailwindcss": "^4" "eslint": "^8",
"eslint-config-next": "14.2.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5"
} }
} }

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,5 +0,0 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

108
src/app/3d-models/page.tsx Normal file
View File

@@ -0,0 +1,108 @@
'use client'
import { useState } from 'react'
import { ModelCard } from '@/components/ModelCard'
import { ModelModal } from '@/components/ModelModal'
import { Model3D } from '@/types'
import Image from 'next/image'
const models: Model3D[] = [
{
id: '1',
title: 'Splitter Mini',
description: 'Open pin locking mechanisms with a simple and concealable non-magnetic tool. Splitter Mini is designed to fit easily in your pocket.',
detailedDescription: 'The splitter mini is an elegant bypass for the notorious locking gray and green phone bags schools across the country have been implementing. While they can be defeated with nothing more than a pen or decently strong magnet, the Splitter Mini makes the brute-force route a easier and safer. No more worrying about straightening bent pins.',
images: [
'/images/splitter-mini/SplitterMini.jpg',
'/images/splitter-mini/SplitterMiniBack.jpg',
'/images/splitter-mini/SplitterMiniThickness.jpg',
'/images/splitter-mini/SplitterMiniWidth.jpg'
],
files: [
{ name: 'Spliter Mini v5.step', type: 'Object', size: '1.8 MB', url: '/models/splitter-mini/Splitter%20Mini%20v5.step' },
{ name: 'Splitter Mini v5 Unbranded.step', type: 'Object', size: '54 KB', url: '/models/splitter-mini/Splitter%20Mini%20v5%20Unbranded.step' },
{ name: 'Splitter Mini v5.f3d', type: 'CAD', size: '958 KB', url: '/models/splitter-mini/Splitter%20Mini%20v5.f3d' }
],
category: 'Accessibility',
},
{
id: '2',
title: 'Splitter XL',
description: 'Open pin locking mechanisms with a simple and concealable non-magnetic tool. Splitter Mini is designed to fit easily in your pocket.',
detailedDescription: 'The splitter mini is an elegant bypass for the notorious locking gray and green phone bags schools across the country have been implementing. While they can be defeated with nothing more than a pen or decently strong magnet, the Splitter Mini makes the brute-force route a easier and safer. No more worrying about straightening bent pins.',
images: [
'/images/splitter-xl/SplitterXL.jpg',
'/images/splitter-xl/SplitterXLThickness.jpg',
'/images/splitter-xl/SplitterXLWidth.jpg'
],
files: [
{ name: 'Spliter XL v6.step', type: 'Object', size: '1.7 MB', url: '/models/splitter-xl/Splitter%20XL%20v6.step' },
{ name: 'Splitter XL v6 Unbranded.step', type: 'Object', size: '55 KB', url: '/models/splitter-xl/Splitter%20XL%20v6%20Unbranded.step' },
{ name: 'Splitter XL v6.f3d', type: 'CAD', size: '900 KB', url: '/models/splitter-xl/Splitter%20XL%20v6.f3d' }
],
category: 'Accessibility',
}
]
export default function Models3D() {
const [selectedModel, setSelectedModel] = useState<Model3D | null>(null)
const [filter, setFilter] = useState<string>('All')
const categories = ['All', 'Accessibility', 'Ergonomics', 'Privacy', 'Organization']
const filteredModels = filter === 'All'
? models
: models.filter(model => model.category === filter)
return (
<div className="min-h-screen py-16 px-4">
<div className="max-w-7xl mx-auto">
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-4xl md:text-6xl font-bold mb-6 text-primary">
3D Models
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-8">
Open-source 3D printable designs for accessibility, privacy, and digital rights
</p>
{/* Filter Tabs */}
<div className="flex flex-wrap justify-center gap-2">
{categories.map((category) => (
<button
key={category}
onClick={() => setFilter(category)}
className={`px-4 py-2 rounded-full transition-colors ${
filter === category
? 'bg-primary text-primary-foreground'
: 'bg-secondary hover:bg-secondary/80'
}`}
>
{category}
</button>
))}
</div>
</div>
{/* Models Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredModels.map((model) => (
<ModelCard
key={model.id}
model={model}
onClick={() => setSelectedModel(model)}
/>
))}
</div>
{/* Modal */}
{selectedModel && (
<ModelModal
model={selectedModel}
onClose={() => setSelectedModel(null)}
/>
)}
</div>
</div>
)
}

View File

@@ -1,26 +1,39 @@
@import "tailwindcss"; @tailwind base;
@tailwind components;
@tailwind utilities;
:root { :root {
--background: #ffffff; --background: 222.2 84% 4.9%;
--foreground: #171717; --foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 94.1%;
} }
@theme inline { * {
--color-background: var(--background); border-color: hsl(var(--border));
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
} }
body { body {
background: var(--background); color: hsl(var(--foreground));
color: var(--foreground); background: hsl(var(--background));
font-family: Arial, Helvetica, sans-serif;
} }
html {
scroll-behavior: smooth;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@@ -1,34 +1,28 @@
import type { Metadata } from "next"; import type { Metadata } from 'next'
import { Geist, Geist_Mono } from "next/font/google"; import { Inter } from 'next/font/google'
import "./globals.css"; import './globals.css'
import { Navigation } from '@/components/Navigation'
const geistSans = Geist({ const inter = Inter({ subsets: ['latin'] })
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: 'DefendAccess.org',
description: "Generated by create next app", description: 'Defending digital access and rights with open source tools',
}; }
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: {
children: React.ReactNode; children: React.ReactNode
}>) { }) {
return ( return (
<html lang="en"> <html lang="en" className="dark">
<body <body className={`${inter.className} min-h-screen bg-background text-foreground`}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`} <Navigation />
> <main className="pt-16">
{children} {children}
</main>
</body> </body>
</html> </html>
); )
} }

View File

@@ -0,0 +1,123 @@
import { ImagePlaceholder } from '@/components/ImagePlaceholder'
export default function OurMission() {
return (
<div className="min-h-screen py-16 px-4">
<div className="max-w-6xl mx-auto">
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-4xl md:text-6xl font-bold mb-6 text-primary">
Our Mission
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Empowering individuals and communities to protect their digital access needs through open-source resources
</p>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center mb-16">
<div className="space-y-6">
<h2 className="text-3xl font-bold text-primary">Defending Access With Innovation</h2>
<p className="text-lg text-muted-foreground leading-relaxed">
Some may argue that mobile devices are a distraction, and with the rise of addictive apps such as TikTok,
Instagram, and Twitter, for some that may be the case. However, for many, cell phones are tools for
efficient work, enhancing one's scheduling, communication, productivity, and more.
</p>
<p className="text-lg text-muted-foreground leading-relaxed">
For students who struggle to focus in noisy or overwhelming settings, mobile access paired with tools like
noise-canceling headphones can be transformative. By filtering out background chatter, HVAC hum, or classroom
commotion, these headphones help students-particularly those with, autism, ADHD, Sensory Processing Disorder, or other auditory sensitivities-to
concentrate more deeply, reduce stress, and stay engaged in learning tasks. Real-world reports and studies
have noted improved task performance, greater calm, and increased participation when noise-canceling headphones
are used as part of a sensory toolkit or personalized learning strategy.
Placeholder Citation: https://learningheadphones.com/blogs/school-headphone-blog/the-impact-of-noise-cancelling-headphones-on-student-focus-and-learning-in-the-classroom will embed later
</p>
<p className="text-lg text-muted-foreground leading-relaxed">
Most schools issue their own devices, however, these devices are often cheap, have limited functionality,
and are surveilled by the district. Often these devices come with strict content filters, blocking many
of the websites students may use to boost their mood/productivity, fight off anxiety, and increase focus.
Without access to tools such as Spotify, Apple Music, or Youtube, we will see a regression in many
students grades and overall wellbeing.
Placeholder Citation: https://pmc.ncbi.nlm.nih.gov/articles/PMC9855069/ will embed later
</p>
</div>
<ImagePlaceholder
height="h-96"
text="Mission Statement Visual"
/>
</div>
{/* Image Gallery */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16">
<ImagePlaceholder
height="h-64"
text="Community Workshop"
/>
<ImagePlaceholder
height="h-64"
text="Technology Training"
/>
<ImagePlaceholder
height="h-64"
text="Digital Rights Event"
/>
<ImagePlaceholder
height="h-64"
text="Open Source Project"
/>
<ImagePlaceholder
height="h-64"
text="Accessibility Initiative"
/>
<ImagePlaceholder
height="h-64"
text="Team Collaboration"
/>
</div>
{/* Values Section */}
<section className="bg-secondary/20 rounded-xl p-8 md:p-12">
<h2 className="text-3xl font-bold text-center mb-12 text-primary">Our Core Values</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="text-center space-y-4">
<div className="w-20 h-20 bg-primary/20 rounded-full mx-auto flex items-center justify-center">
<span className="text-3xl">🛡</span>
</div>
<h3 className="text-xl font-semibold">Protection</h3>
<p className="text-muted-foreground">
Safeguarding not just digital access rights, but online privacy for everyone.
</p>
</div>
<div className="text-center space-y-4">
<div className="w-20 h-20 bg-primary/20 rounded-full mx-auto flex items-center justify-center">
<span className="text-3xl">🌐</span>
</div>
<h3 className="text-xl font-semibold">Accessibility</h3>
<p className="text-muted-foreground">
Ensuring technology is accessible and usable by people of all abilities.
</p>
</div>
<div className="text-center space-y-4">
<div className="w-20 h-20 bg-primary/20 rounded-full mx-auto flex items-center justify-center">
<span className="text-3xl">🔓</span>
</div>
<h3 className="text-xl font-semibold">Openness</h3>
<p className="text-muted-foreground">
Promoting open source solutions and transparent practices.
</p>
</div>
<div className="text-center space-y-4">
<div className="w-20 h-20 bg-primary/20 rounded-full mx-auto flex items-center justify-center">
<span className="text-3xl">🤝</span>
</div>
<h3 className="text-xl font-semibold">Community</h3>
<p className="text-muted-foreground">
Building strong communities through collaboration and shared knowledge.
</p>
</div>
</div>
</section>
</div>
</div>
)
}

View File

@@ -1,103 +1,107 @@
import Image from "next/image"; "use client";
import { useState } from "react";
import { Hero } from "@/components/Hero";
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
const reasons = [
{
title: "Barriers to Focused Work",
content:
"Some may argue that mobile devices are a distraction, and with the rise of addictive apps such as TikTok, Instagram, and Twitter, for some that may be the case. However, for many, cell phones are tools for efficient work, enhancing one's scheduling, communication, productivity, and more.",
},
{
title: "Limited Access to Appropriate Tools",
content:
"The bill permits schools to issue their own devices, however, these devices are often cheap, have limited functionality, and are surveilled by the district. Often these devices come with strict content filters, blocking many of the websites students may use to boost their productivity, such as Spotify, Apple Music, or Youtube.",
},
{
title: "Placeholder Title",
content:
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.",
},
{
title: "Placeholder Title",
content:
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium.",
},
];
export default function Home() { export default function Home() {
return ( // Toggle to show/hide modal (set to true during development)
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"> const [showModal, setShowModal] = useState(true);
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start"> const [isChecked, setIsChecked] = useState(false);
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row"> return (
<a <div className="min-h-screen relative">
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto" {/* Modal Overlay */}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" {showModal && (
target="_blank" <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-70">
rel="noopener noreferrer" <div className="bg-gray-900 text-gray-100 rounded-lg shadow-lg p-6 max-w-lg w-full mx-4">
> <h2 className="text-2xl font-bold mb-4 text-white">Disclaimer</h2>
<Image <p className="text-gray-300 mb-4">
className="dark:invert" This website is still under development. All content is subject
src="/vercel.svg" to change. Placeholder text and images may be visible.
alt="Vercel logomark" </p>
width={20}
height={20} <label className="flex items-center space-x-2 mb-4">
/> <input
Deploy now type="checkbox"
</a> checked={isChecked}
<a onChange={(e) => setIsChecked(e.target.checked)}
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]" className="h-4 w-4 text-blue-500 bg-gray-800 border-gray-600 rounded focus:ring-blue-500 focus:ring-offset-gray-900"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" />
target="_blank" <span className="text-gray-200">I understand and wish to proceed.</span>
rel="noopener noreferrer" </label>
>
Read our docs <button
</a> onClick={() => setShowModal(false)}
disabled={!isChecked}
className={`px-4 py-2 rounded text-white ${
isChecked
? "bg-blue-600 hover:bg-blue-700"
: "bg-gray-600 cursor-not-allowed"
}`}
>
Proceed
</button>
</div>
</div> </div>
</main> )}
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a {/* Main Page Content */}
className="flex items-center gap-2 hover:underline hover:underline-offset-4" <Hero />
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank" <section className="py-16 px-4">
rel="noopener noreferrer" <div className="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12">
> <div className="space-y-8">
<Image <h2 className="text-3xl font-bold mb-6 text-primary">
aria-hidden Why DefendAccess Matters
src="/file.svg" </h2>
alt="File icon" <p className="text-lg text-muted-foreground leading-relaxed mb-6">
width={16} Not everyone has equal needs, tolerances, and preferences when it
height={16} comes to learning, working, and living. Some people may be
/> detrimented by the presence of technology, while others may be
Learn empowered by it.
</a> </p>
<a <p className="text-lg text-muted-foreground leading-relaxed mb-6">
className="flex items-center gap-2 hover:underline hover:underline-offset-4" This bill will require all public schools to adopt policies preventing
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" students from using any personal electronic devices. While written in
target="_blank" good faith, the bill creates more hoops for students with needs to jump
rel="noopener noreferrer" through, and will negatively impact those without access to offical
> diagnoises or accomodations.
<Image </p>
aria-hidden </div>
src="/window.svg"
alt="Window icon" <div className="space-y-6">
width={16} <ImagePlaceholder height="h-64" text="Featured Initiative Image" />
height={16} <div className="grid grid-cols-2 gap-4">
/> <ImagePlaceholder height="h-32" text="Community Photo" />
Examples <ImagePlaceholder height="h-32" text="Technology Demo" />
</a> </div>
<a </div>
className="flex items-center gap-2 hover:underline hover:underline-offset-4" </div>
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" </section>
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div> </div>
); );
} }

View File

@@ -0,0 +1,30 @@
'use client'
import { useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
interface ExpandableSectionProps {
title: string
content: string
}
export function ExpandableSection({ title, content }: ExpandableSectionProps) {
const [isExpanded, setIsExpanded] = useState(false)
return (
<div className="border border-border rounded-lg overflow-hidden">
<button
className="w-full px-6 py-4 text-left bg-secondary/50 hover:bg-secondary/70 transition-colors flex items-center justify-between"
onClick={() => setIsExpanded(!isExpanded)}
>
<h3 className="font-semibold">{title}</h3>
{isExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</button>
{isExpanded && (
<div className="px-6 py-4 bg-background animate-slide-up">
<p className="text-muted-foreground leading-relaxed">{content}</p>
</div>
)}
</div>
)
}

37
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,37 @@
export function Hero() {
return (
<section className="relative h-screen flex items-center justify-center overflow-hidden">
{/* Background Animation */}
<div className="absolute inset-0 opacity-20">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-primary/100 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-secondary/100 rounded-full blur-3xl animate-pulse delay-1000"></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-128 h-128 bg-accent/100 rounded-full blur-3xl animate-rotate"></div>
</div>
<div className="relative z-10 text-center px-4">
<h1 className="text-5xl md:text-7xl font-bold mb-6 animate-fade-in">
<span className="text-primary">Defend</span>
<span className="text-foreground">Access</span>
</h1>
<p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto mb-8 animate-slide-up">
Empowering individuals and communities to protect their digital access needs through open-source resources
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center animate-slide-up delay-200">
<button className="px-8 py-4 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-semibold">
Explore Our Mission
</button>
<button className="px-8 py-4 border border-border text-foreground rounded-lg hover:bg-secondary/50 transition-colors font-semibold">
Download 3D Models
</button>
</div>
</div>
{/* Scroll Indicator */}
<div className="absolute bottom-32 left-1/2 transform -translate-x-1/2 animate-bounce">
<div className="w-6 h-10 border-2 border-muted-foreground rounded-full flex justify-center">
<div className="w-1 h-3 bg-muted-foreground rounded-full mt-2 animate-pulse"></div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,13 @@
interface ImagePlaceholderProps {
height: string
text: string
className?: string
}
export function ImagePlaceholder({ height, text, className = '' }: ImagePlaceholderProps) {
return (
<div className={`${height} bg-secondary/30 rounded-lg flex items-center justify-center border-2 border-dashed border-border ${className}`}>
<span className="text-muted-foreground font-medium text-center px-4">{text}</span>
</div>
)
}

View File

@@ -0,0 +1,114 @@
'use client'
import { useState } from 'react'
import { Download, Eye, ChevronLeft, ChevronRight } from 'lucide-react'
import { Model3D } from '@/types'
import { ImagePlaceholder } from './ImagePlaceholder'
import Image from 'next/image'
interface ModelCardProps {
model: Model3D
onClick: () => void
}
export function ModelCard({ model, onClick }: ModelCardProps) {
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const nextImage = (e: React.MouseEvent) => {
e.stopPropagation()
setCurrentImageIndex((prev) => (prev + 1) % model.images.length)
}
const prevImage = (e: React.MouseEvent) => {
e.stopPropagation()
setCurrentImageIndex((prev) => (prev - 1 + model.images.length) % model.images.length)
}
return (
<div className="bg-secondary/20 rounded-xl overflow-hidden hover:bg-secondary/30 transition-all duration-300 cursor-pointer group">
{/* Image Gallery */}
<div className="relative h-48 overflow-hidden">
<Image
src={model.images[currentImageIndex]}
alt={model.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{/* Image Navigation */}
{model.images.length > 1 && (
<>
<button
onClick={prevImage}
className="absolute left-2 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-2 rounded-full transition-all opacity-0 group-hover:opacity-100"
>
<ChevronLeft size={16} />
</button>
<button
onClick={nextImage}
className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-2 rounded-full transition-all opacity-0 group-hover:opacity-100"
>
<ChevronRight size={16} />
</button>
{/* Image Indicators */}
<div className="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex space-x-1">
{model.images.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full transition-all ${
index === currentImageIndex ? 'bg-white' : 'bg-white/50'
}`}
/>
))}
</div>
</>
)}
</div>
{/* Card Content */}
<div className="p-6">
<div className="flex items-start justify-between mb-3">
<h3 className="text-xl font-semibold text-foreground group-hover:text-primary transition-colors">
{model.title}
</h3>
</div>
<div className="flex items-center gap-2 mb-3">
<span className="px-2 py-1 bg-primary/20 text-primary rounded-full text-xs font-medium">
{model.category}
</span>
<span className="text-sm text-muted-foreground">
{model.files.length} files
</span>
</div>
<p className="text-muted-foreground text-sm leading-relaxed mb-4 line-clamp-3">
{model.description}
</p>
{/* Action Buttons */}
<div className="flex gap-2">
<button
onClick={onClick}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium"
>
<Eye size={16} />
View Details
</button>
{model.files.length > 0 && (
<a
href={model.files[0].url}
download
onClick={(e) => e.stopPropagation()} // prevents opening modal
className="px-4 py-2 border border-border rounded-lg hover:bg-secondary/50 transition-colors text-sm font-medium flex items-center gap-2"
>
<Download size={16} />
Quick Download
</a>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,228 @@
'use client'
import { useState, useEffect } from 'react'
import { X, Download, ChevronLeft, ChevronRight, FileText, Package, Settings } from 'lucide-react'
import { Model3D, ModelFile } from '@/types'
import { ImagePlaceholder } from './ImagePlaceholder'
import Image from 'next/image'
import JSZip from 'jszip'
import { saveAs } from 'file-saver'
interface ModelModalProps {
model: Model3D
onClose: () => void
}
export function ModelModal({ model, onClose }: ModelModalProps) {
const [currentImageIndex, setCurrentImageIndex] = useState(0)
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleEscape)
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = 'unset'
}
}, [onClose])
const nextImage = () => {
setCurrentImageIndex((prev) => (prev + 1) % model.images.length)
}
const prevImage = () => {
setCurrentImageIndex((prev) => (prev - 1 + model.images.length) % model.images.length)
}
const getFileIcon = (type: ModelFile['type']) => {
switch (type) {
case 'Object': return <Package className="w-5 h-5" />
case 'CAD': return <Settings className="w-5 h-5" />
case 'Profile': return <FileText className="w-5 h-5" />
case 'Guide': return <FileText className="w-5 h-5" />
default: return <FileText className="w-5 h-5" />
}
}
const getFileTypeColor = (type: ModelFile['type']) => {
switch (type) {
case 'Object': return 'bg-blue-500/20 text-blue-400'
case 'CAD': return 'bg-purple-500/20 text-purple-400'
case 'Profile': return 'bg-green-500/20 text-green-400'
case 'Guide': return 'bg-orange-500/20 text-orange-400'
default: return 'bg-gray-500/20 text-gray-400'
}
}
const downloadAllFiles = async () => {
const zip = new JSZip()
for (const file of model.files) {
try {
const response = await fetch(file.url)
if (!response.ok) throw new Error(`Failed to fetch ${file.name}`)
const blob = await response.blob()
zip.file(file.name, blob)
} catch (err) {
console.error(`Error adding ${file.name} to zip:`, err)
}
}
zip.generateAsync({ type: 'blob' }).then((content) => {
saveAs(content, `${model.title.replace(/\s+/g, '_')}_files.zip`)
})
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-background border border-border rounded-xl max-w-6xl max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-background border-b border-border p-6 flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-foreground">{model.title}</h2>
<div className="flex items-center gap-3 mt-2">
<span className="px-3 py-1 bg-primary/20 text-primary rounded-full text-sm font-medium">
{model.category}
</span>
<span className="text-sm text-muted-foreground">
{model.files.length} files available
</span>
</div>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-secondary rounded-lg transition-colors"
>
<X size={24} />
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 p-6">
{/* Image Gallery */}
<div>
<div className="relative mb-4">
<Image
fill
src={model.images[currentImageIndex]}
alt={model.title}
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{/* Navigation Arrows */}
{model.images.length > 1 && (
<>
<button
onClick={prevImage}
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full transition-all"
>
<ChevronLeft size={20} />
</button>
<button
onClick={nextImage}
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full transition-all"
>
<ChevronRight size={20} />
</button>
</>
)}
</div>
{/* Thumbnail Gallery */}
{model.images.length > 1 && (
<div className="grid grid-cols-4 gap-2">
{model.images.map((image, index) => (
<button
key={index}
onClick={() => setCurrentImageIndex(index)}
className={`relative ${
index === currentImageIndex ? 'ring-2 ring-primary' : 'opacity-70 hover:opacity-100'
} transition-all rounded-lg overflow-hidden`}
>
<Image
src={image}
alt={`${model.title} thumbnail ${index + 1}`}
width={80}
height={80}
className="object-cover"
/>
</button>
))}
</div>
)}
</div>
{/* Content */}
<div className="space-y-6">
{/* Description */}
<div>
<h3 className="text-lg font-semibold mb-3 text-foreground">Description</h3>
<p className="text-muted-foreground leading-relaxed mb-4">
{model.description}
</p>
<p className="text-muted-foreground leading-relaxed">
{model.detailedDescription}
</p>
</div>
{/* Files */}
<div>
<h3 className="text-lg font-semibold mb-4 text-foreground">Available Files</h3>
<div className="space-y-3">
{model.files.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-4 bg-secondary/20 rounded-lg border border-border hover:bg-secondary/30 transition-colors"
>
<div className="flex items-center gap-3">
{getFileIcon(file.type)}
<div>
<p className="font-medium text-foreground">{file.name}</p>
<div className="flex items-center gap-2 mt-1">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getFileTypeColor(file.type)}`}>
{file.type}
</span>
<span className="text-sm text-muted-foreground">{file.size}</span>
</div>
</div>
</div>
<a
href={file.url}
download
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium"
>
<Download size={16} />
Download
</a>
</div>
))}
</div>
</div>
{/* Download All Button */}
<button
onClick={downloadAllFiles}
className="w-full flex items-center justify-center gap-2 px-6 py-4 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-semibold text-lg"
>
<Download size={20} />
Download All Files
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,75 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { Menu, X } from 'lucide-react'
const navItems = [
{ name: 'Home', href: '/' },
{ name: 'Our Mission', href: '/our-mission' },
{ name: '3D Models', href: '/3d-models' },
]
export function Navigation() {
const pathname = usePathname()
const [isMenuOpen, setIsMenuOpen] = useState(false)
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<Link href="/" className="text-2xl font-bold text-primary">
DefendAccess.org
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-8">
{navItems.map((item) => (
<Link
key={item.name}
href={item.href}
className={`transition-colors hover:text-primary ${
pathname === item.href
? 'text-primary border-b-2 border-primary'
: 'text-muted-foreground'
}`}
>
{item.name}
</Link>
))}
</div>
{/* Mobile Menu Button */}
<button
className="md:hidden p-2"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Mobile Navigation */}
{isMenuOpen && (
<div className="md:hidden py-4 border-t border-border">
{navItems.map((item) => (
<Link
key={item.name}
href={item.href}
className={`block py-2 transition-colors hover:text-primary ${
pathname === item.href
? 'text-primary'
: 'text-muted-foreground'
}`}
onClick={() => setIsMenuOpen(false)}
>
{item.name}
</Link>
))}
</div>
)}
</div>
</nav>
)
}

16
src/types/index.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface ModelFile {
name: string
type: 'Object' | 'CAD' | 'Profile' | 'Guide'
size: string
url: string
}
export interface Model3D {
id: string
title: string
description: string
detailedDescription: string
images: string[]
files: ModelFile[]
category: string
}

57
tailwind.config.ts Normal file
View File

@@ -0,0 +1,57 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
},
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.5s ease-out',
'rotate': 'rotate 20s linear infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
rotate: {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
},
},
},
plugins: [],
}
export default config

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "es6"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -18,10 +18,11 @@
"name": "next" "name": "next"
} }
], ],
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }