🚀 feat(_new): new website ui and function

This commit is contained in:
eshanized
2024-12-25 03:38:05 +05:30
parent 3196782ce5
commit 3e920da9fc
89 changed files with 10212 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import { motion } from 'framer-motion';
import { Target } from 'lucide-react';
export function MissionSection() {
return (
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
className="bg-white/80 backdrop-blur-sm p-8 rounded-lg shadow-sm"
>
<div className="flex items-center gap-3 mb-6">
<Target className="h-6 w-6 text-cornflower-blue" />
<h2 className="text-2xl font-bold text-gray-900">Our Mission</h2>
</div>
<div className="prose prose-gray max-w-none">
<p className="text-gray-600 leading-relaxed">
Snigdha OS aims to provide security professionals and enthusiasts with the most comprehensive,
reliable, and up-to-date collection of security tools. Our mission is to enable the security
community to perform professional-grade security auditing and penetration testing with a
standardized, well-documented platform.
</p>
<h3 className="text-xl font-semibold mt-6 mb-3">Core Values</h3>
<ul className="space-y-2 text-gray-600">
<li>Open Source: Maintaining transparency and community collaboration</li>
<li>Security: Providing robust tools for security testing</li>
<li>Education: Supporting learning and skill development</li>
<li>Community: Fostering a strong, supportive user community</li>
</ul>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,43 @@
import { motion } from 'framer-motion';
import { Users } from 'lucide-react';
const teamStructure = [
{
title: 'Core Development',
description: 'Responsible for the base system and core tools integration',
},
{
title: 'Security Tools',
description: 'Maintains and updates the vast collection of security tools',
},
{
title: 'Documentation',
description: 'Creates and maintains user documentation and guides',
},
{
title: 'Community Management',
description: 'Manages community interactions and contributions',
},
];
export function TeamSection() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{teamStructure.map((team, index) => (
<motion.div
key={team.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="bg-white/80 backdrop-blur-sm p-6 rounded-lg shadow-sm"
>
<div className="flex items-center gap-3 mb-3">
<Users className="h-5 w-5 text-cornflower-blue" />
<h3 className="text-lg font-semibold text-gray-900">{team.title}</h3>
</div>
<p className="text-gray-600">{team.description}</p>
</motion.div>
))}
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { motion } from 'framer-motion';
import { Calendar } from 'lucide-react';
const releases = [
{ version: '2024.1', date: '2024', description: 'Latest release with enhanced cloud support' },
{ version: '2023.4', date: '2023', description: 'Major UI overhaul and tool updates' },
{ version: '2023.1', date: '2023', description: 'Introduced new wireless testing tools' },
{ version: '2022.4', date: '2022', description: 'Added ARM64 support improvements' },
{ version: '2022.1', date: '2022', description: 'Enhanced container support' },
];
export function Timeline() {
return (
<div className="relative">
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-200" />
{releases.map((release, index) => (
<motion.div
key={release.version}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="relative pl-12 pb-8"
>
<div className="absolute left-0 p-2 bg-white rounded-full border-2 border-cornflower-blue">
<Calendar className="h-4 w-4 text-cornflower-blue" />
</div>
<div className="bg-white/80 backdrop-blur-sm p-4 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-gray-900">
Kali {release.version}
</h3>
<time className="text-sm text-gray-500">{release.date}</time>
<p className="mt-1 text-gray-600">{release.description}</p>
</div>
</motion.div>
))}
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { motion } from 'framer-motion';
import { Github, GitCommit } from 'lucide-react';
interface ContributorCardProps {
login: string;
avatarUrl: string;
contributions: number;
profileUrl: string;
}
export function ContributorCard({ login, avatarUrl, contributions, profileUrl }: ContributorCardProps) {
return (
<motion.div
whileHover={{ y: -5 }}
className="bg-white/80 backdrop-blur-sm p-6 rounded-lg border border-gray-200"
>
<div className="flex items-center gap-4">
<img
src={avatarUrl}
alt={`${login}'s avatar`}
className="w-16 h-16 rounded-full"
/>
<div>
<h3 className="font-semibold text-gray-900">{login}</h3>
<div className="flex items-center gap-2 text-sm text-gray-600">
<GitCommit className="h-4 w-4" />
<span>{contributions} contributions</span>
</div>
</div>
</div>
<a
href={profileUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-4 flex items-center gap-2 text-sm text-cornflower-blue hover:underline"
>
<Github className="h-4 w-4" />
View Profile
</a>
</motion.div>
);
}

View File

@@ -0,0 +1,25 @@
import { Users, GitPullRequest, GitCommit } from 'lucide-react';
interface Stats {
totalContributors: number;
totalPullRequests: number;
totalCommits: number;
}
export function ContributorStats({ totalContributors, totalPullRequests, totalCommits }: Stats) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[
{ icon: Users, label: 'Contributors', value: totalContributors },
{ icon: GitPullRequest, label: 'Pull Requests', value: totalPullRequests },
{ icon: GitCommit, label: 'Commits', value: totalCommits },
].map(({ icon: Icon, label, value }) => (
<div key={label} className="bg-white/80 backdrop-blur-sm p-6 rounded-lg">
<Icon className="h-8 w-8 text-cornflower-blue mb-2" />
<p className="text-2xl font-bold text-gray-900">{value.toLocaleString()}</p>
<p className="text-sm text-gray-600">{label}</p>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,56 @@
import { motion } from 'framer-motion';
import { Star, GitFork, Clock } from 'lucide-react';
import { formatDate } from '@/lib/utils';
import type { GithubRepo } from '@/lib/github';
interface RepoCardProps {
repo: GithubRepo;
}
export function RepoCard({ repo }: RepoCardProps) {
return (
<motion.div
whileHover={{ y: -5 }}
className="bg-white/80 backdrop-blur-sm p-6 rounded-lg border border-gray-200"
>
<h3 className="text-lg font-semibold text-gray-900">
<a
href={repo.html_url}
target="_blank"
rel="noopener noreferrer"
className="hover:text-cornflower-blue transition-colors"
>
{repo.name}
</a>
</h3>
{repo.description && (
<p className="mt-2 text-gray-600 line-clamp-2">{repo.description}</p>
)}
<div className="mt-4 flex items-center gap-4 text-sm text-gray-500">
{repo.language && (
<span className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full bg-cornflower-blue" />
{repo.language}
</span>
)}
<span className="flex items-center gap-1">
<Star className="h-4 w-4" />
{repo.stargazers_count}
</span>
<span className="flex items-center gap-1">
<GitFork className="h-4 w-4" />
{repo.forks_count}
</span>
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{formatDate(repo.updated_at)}
</span>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,58 @@
import { motion } from 'framer-motion';
import { MapPin, Users, Book } from 'lucide-react';
import type { GithubUser } from '@/lib/github';
interface TeamMemberCardProps {
user: GithubUser;
role: string;
description: string;
}
export function TeamMemberCard({ user, role, description }: TeamMemberCardProps) {
return (
<motion.div
whileHover={{ y: -5 }}
className="bg-white/80 backdrop-blur-sm p-6 rounded-lg border border-gray-200"
>
<div className="flex items-start gap-4">
<img
src={user.avatar_url}
alt={`${user.login}'s avatar`}
className="w-16 h-16 rounded-full"
/>
<div>
<h3 className="font-semibold text-gray-900">{user.name || user.login}</h3>
<p className="text-sm text-cornflower-blue">{role}</p>
{user.location && (
<div className="flex items-center gap-1 text-sm text-gray-500 mt-1">
<MapPin className="h-4 w-4" />
<span>{user.location}</span>
</div>
)}
</div>
</div>
<p className="mt-4 text-gray-600">{description}</p>
<div className="mt-4 flex items-center gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Users className="h-4 w-4" />
<span>{user.followers.toLocaleString()} followers</span>
</div>
<div className="flex items-center gap-1">
<Book className="h-4 w-4" />
<span>{user.public_repos} repos</span>
</div>
</div>
<a
href={user.html_url}
target="_blank"
rel="noopener noreferrer"
className="mt-4 inline-flex items-center text-sm text-cornflower-blue hover:underline"
>
View GitHub Profile
</a>
</motion.div>
);
}

View File

@@ -0,0 +1,46 @@
import { motion } from 'framer-motion';
import { Check } from 'lucide-react';
interface DonationTierProps {
name: string;
amount: number;
benefits: string[];
recommended?: boolean;
onSelect: () => void;
}
export function DonationTier({ name, amount, benefits, recommended, onSelect }: DonationTierProps) {
return (
<motion.div
whileHover={{ y: -5 }}
className={`bg-white/80 backdrop-blur-sm p-6 rounded-lg border ${
recommended ? 'border-cornflower-blue' : 'border-gray-200'
}`}
>
{recommended && (
<span className="inline-block px-3 py-1 text-xs font-medium text-cornflower-blue bg-blue-50 rounded-full mb-4">
Recommended
</span>
)}
<h3 className="text-xl font-semibold text-gray-900">{name}</h3>
<p className="mt-2 text-3xl font-bold text-gray-900">${amount}</p>
<ul className="mt-6 space-y-3">
{benefits.map((benefit) => (
<li key={benefit} className="flex items-start gap-2">
<Check className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-gray-600">{benefit}</span>
</li>
))}
</ul>
<button
onClick={onSelect}
className="mt-8 w-full py-2 px-4 bg-cornflower-blue text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Select
</button>
</motion.div>
);
}

View File

@@ -0,0 +1,45 @@
import { motion } from 'framer-motion';
import { Heart } from 'lucide-react';
interface Donor {
name: string;
amount: number;
message?: string;
date: string;
}
interface DonorWallProps {
donors: Donor[];
}
export function DonorWall({ donors }: DonorWallProps) {
return (
<div className="bg-white/80 backdrop-blur-sm p-6 rounded-lg">
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Recent Supporters</h2>
<div className="space-y-6">
{donors.map((donor, index) => (
<motion.div
key={`${donor.name}-${donor.date}`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-start gap-4"
>
<Heart className="h-5 w-5 text-red-500 flex-shrink-0 mt-1" />
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium text-gray-900">{donor.name}</h3>
<span className="text-sm text-cornflower-blue">${donor.amount}</span>
</div>
{donor.message && (
<p className="mt-1 text-sm text-gray-600">{donor.message}</p>
)}
<time className="text-xs text-gray-500">{donor.date}</time>
</div>
</motion.div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import { Github } from 'lucide-react';
import { motion } from 'framer-motion';
export function GithubSponsorButton() {
return (
<motion.a
href="https://github.com/sponsors/eshanized"
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-flex items-center gap-2 px-6 py-3 bg-[#2ea44f] text-white rounded-lg hover:bg-[#2c974b] transition-colors"
>
<Github className="h-5 w-5" />
Sponsor on GitHub
</motion.a>
);
}

View File

@@ -0,0 +1,59 @@
import { motion } from 'framer-motion';
import { Building2 } from 'lucide-react';
import { useQueries } from '@tanstack/react-query';
import { fetchGithubUser } from '@/lib/github';
import { keySponsors } from '@/data/sponsors';
import { formatINR } from '@/lib/currency';
export function KeySponsors() {
const queries = useQueries({
queries: keySponsors.map(sponsor => ({
queryKey: ['github-user', sponsor.githubUsername],
queryFn: () => fetchGithubUser(sponsor.githubUsername),
})),
});
return (
<div className="bg-gradient-to-r from-cornflower-blue/5 to-blue-50/50 rounded-2xl p-8">
<h2 className="text-2xl font-bold text-gray-900 text-center mb-8">
Key Sponsors
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{keySponsors.map((sponsor, index) => {
const query = queries[index];
return (
<motion.div
key={sponsor.name}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="bg-white/90 backdrop-blur-sm rounded-xl p-6 border border-cornflower-blue/20 shadow-sm"
>
<div className="flex items-center gap-4">
{query.data ? (
<img
src={query.data.avatar_url}
alt={`${sponsor.name}'s profile`}
className="w-12 h-12 rounded-full"
/>
) : (
<div className="p-3 bg-cornflower-blue/10 rounded-lg">
<Building2 className="h-8 w-8 text-cornflower-blue" />
</div>
)}
<div>
<h3 className="font-semibold text-gray-900">{sponsor.name}</h3>
<p className="text-sm text-gray-600">{sponsor.description}</p>
<p className="text-sm font-medium text-cornflower-blue mt-1">
{formatINR(sponsor.amount)}/month
</p>
</div>
</div>
</motion.div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { motion } from 'framer-motion';
import { Building2 } from 'lucide-react';
import { type UseQueryResult } from '@tanstack/react-query';
import { type GithubUser } from '@/lib/github';
import { type Sponsor } from '@/types/sponsor';
import { formatINR } from '@/lib/currency';
interface SponsorCardProps {
sponsor: Sponsor;
query: UseQueryResult<GithubUser>;
index: number;
}
export function SponsorCard({ sponsor, query, index }: SponsorCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="bg-white/90 backdrop-blur-sm rounded-xl p-6 border border-cornflower-blue/20 shadow-sm"
>
<div className="flex items-center gap-4">
{query.data && !query.error ? (
<img
src={query.data.avatar_url}
alt={`${sponsor.name}'s profile`}
className="w-12 h-12 rounded-full"
/>
) : (
<div className="p-3 bg-cornflower-blue/10 rounded-lg">
<Building2 className="h-8 w-8 text-cornflower-blue" />
</div>
)}
<div>
<h3 className="font-semibold text-gray-900">{sponsor.name}</h3>
<p className="text-sm text-gray-600">{sponsor.description}</p>
<p className="text-sm font-medium text-cornflower-blue mt-1">
{formatINR(sponsor.amount)}/month
</p>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,33 @@
import { useQueries } from '@tanstack/react-query';
import { fetchGithubUser } from '@/lib/github';
import { keySponsors } from '@/data/sponsors';
import { SponsorCard } from './SponsorCard';
export function KeySponsors() {
const queries = useQueries({
queries: keySponsors.map(sponsor => ({
queryKey: ['github-user', sponsor.githubUsername],
queryFn: () => fetchGithubUser(sponsor.githubUsername),
retry: 1, // Only retry once to avoid excessive API calls
staleTime: 1000 * 60 * 5, // Cache for 5 minutes
})),
});
return (
<div className="bg-gradient-to-r from-cornflower-blue/5 to-blue-50/50 rounded-2xl p-8">
<h2 className="text-2xl font-bold text-gray-900 text-center mb-8">
Key Sponsors
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{keySponsors.map((sponsor, index) => (
<SponsorCard
key={sponsor.name}
sponsor={sponsor}
query={queries[index]}
index={index}
/>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { motion } from 'framer-motion';
import { Users, Star, Heart } from 'lucide-react';
import { formatINR } from '@/lib/currency';
const stats = [
{
label: 'Current Sponsors',
value: '50+',
icon: Users,
color: 'text-blue-500'
},
{
label: 'Monthly Support',
value: formatINR(200000),
icon: Heart,
color: 'text-red-500'
},
{
label: 'GitHub Stars',
value: '1.2K+',
icon: Star,
color: 'text-yellow-500'
}
];
export function SponsorshipStats() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{stats.map((stat, index) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="bg-white/80 backdrop-blur-sm p-6 rounded-xl border border-gray-200"
>
<stat.icon className={`h-8 w-8 ${stat.color} mb-2`} />
<p className="text-2xl font-bold text-gray-900">{stat.value}</p>
<p className="text-gray-600">{stat.label}</p>
</motion.div>
))}
</div>
);
}

View File

@@ -0,0 +1,98 @@
import { motion } from 'framer-motion';
import { Check } from 'lucide-react';
import { formatINR } from '@/lib/currency';
const tiers = [
{
name: 'Community Hero',
amount: 399,
description: 'Support the ongoing development of Snigdha OS',
benefits: [
'Special recognition on our GitHub repository',
'Access to sponsor-only updates',
'Vote on feature priorities'
]
},
{
name: 'Security Champion',
amount: 799,
description: 'Help shape the future of security testing',
benefits: [
'All Community Hero benefits',
'Early access to new features',
'Priority support on GitHub',
'Exclusive security tips newsletter'
],
featured: true
},
{
name: 'Enterprise Partner',
amount: 1999,
description: 'Perfect for organizations using Snigdha OS',
benefits: [
'All Security Champion benefits',
'Custom support channel',
'Training materials access',
'Team collaboration features',
'Priority feature requests'
]
}
];
export function SponsorshipTiers() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{tiers.map((tier) => (
<motion.div
key={tier.name}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
className={`relative rounded-2xl ${
tier.featured
? 'bg-gradient-to-b from-cornflower-blue/10 to-cornflower-blue/5 border-2 border-cornflower-blue'
: 'bg-white/80 border border-gray-200'
} backdrop-blur-sm p-6 shadow-lg`}
>
{tier.featured && (
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
<span className="px-4 py-1 bg-cornflower-blue text-white text-sm rounded-full">
Most Popular
</span>
</div>
)}
<div className="mb-4">
<h3 className="text-xl font-semibold text-gray-900">{tier.name}</h3>
<div className="mt-2">
<span className="text-3xl font-bold text-gray-900">{formatINR(tier.amount)}</span>
<span className="text-gray-600">/month</span>
</div>
<p className="mt-2 text-gray-600">{tier.description}</p>
</div>
<ul className="space-y-3 mb-6">
{tier.benefits.map((benefit) => (
<li key={benefit} className="flex items-start gap-2">
<Check className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-gray-600">{benefit}</span>
</li>
))}
</ul>
<a
href={`https://github.com/sponsors/eshanized?frequency=monthly&sponsor=${encodeURIComponent(tier.name)}`}
target="_blank"
rel="noopener noreferrer"
className={`block w-full text-center py-2 px-4 rounded-lg transition-colors ${
tier.featured
? 'bg-cornflower-blue text-white hover:bg-blue-600'
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
}`}
>
Become a {tier.name}
</a>
</motion.div>
))}
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { Shield } from 'lucide-react';
interface ChecksumProps {
sha256: string;
gpg: string;
}
export function Checksum({ sha256, gpg }: ChecksumProps) {
return (
<div className="bg-white/80 backdrop-blur-sm p-6 rounded-lg">
<div className="flex items-center gap-2 mb-4">
<Shield className="h-5 w-5 text-cornflower-blue" />
<h2 className="text-xl font-semibold text-gray-900">Verify Download</h2>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-700 mb-1">SHA256 Checksum</h3>
<code className="block p-2 bg-gray-100 rounded text-sm break-all">{sha256}</code>
</div>
<div>
<h3 className="text-sm font-medium text-gray-700 mb-1">GPG Signature</h3>
<code className="block p-2 bg-gray-100 rounded text-sm break-all">{gpg}</code>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { Download } from 'lucide-react';
import { motion } from 'framer-motion';
interface DownloadButtonProps {
version: string;
size: string;
url: string;
}
export function DownloadButton({ version, size, url }: DownloadButtonProps) {
return (
<motion.a
href={url}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="flex items-center justify-between w-full p-4 bg-white/80 backdrop-blur-sm rounded-lg border border-gray-200 hover:border-cornflower-blue transition-colors"
>
<div>
<h3 className="font-semibold text-gray-900">Snigdha OS {version}</h3>
<p className="text-sm text-gray-500">{size}</p>
</div>
<Download className="h-5 w-5 text-cornflower-blue" />
</motion.a>
);
}

View File

@@ -0,0 +1,48 @@
import { motion } from 'framer-motion';
import { Globe, Wifi, Download } from 'lucide-react';
import { type Mirror } from '@/types/download';
import { formatSpeed } from '@/lib/utils';
interface MirrorListProps {
mirrors: Mirror[];
onSelect: (mirror: Mirror) => void;
}
export function MirrorList({ mirrors, onSelect }: MirrorListProps) {
return (
<div className="space-y-4">
{mirrors.map((mirror, index) => (
<motion.div
key={mirror.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="bg-white/80 backdrop-blur-sm p-4 rounded-lg border border-gray-200 hover:border-cornflower-blue transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Globe className="h-5 w-5 text-cornflower-blue" />
<div>
<h3 className="font-medium text-gray-900">{mirror.name}</h3>
<p className="text-sm text-gray-500">{mirror.location}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-1 text-sm text-gray-600">
<Wifi className="h-4 w-4" />
{formatSpeed(mirror.speed)}
</div>
<button
onClick={() => onSelect(mirror)}
className="flex items-center gap-2 px-4 py-2 bg-cornflower-blue text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<Download className="h-4 w-4" />
Select
</button>
</div>
</div>
</motion.div>
))}
</div>
);
}

View File

@@ -0,0 +1,39 @@
import { Wifi } from 'lucide-react';
interface NetworkSpeedProps {
speed: number;
}
export function NetworkSpeed({ speed }: NetworkSpeedProps) {
const getRecommendation = (speed: number) => {
if (speed >= 100) return 'Excellent for fast downloads';
if (speed >= 50) return 'Good for normal downloads';
if (speed >= 20) return 'Moderate speed, downloads may take longer';
return 'Slow connection, consider using a different mirror';
};
const getSpeedClass = (speed: number) => {
if (speed >= 100) return 'text-green-500';
if (speed >= 50) return 'text-blue-500';
if (speed >= 20) return 'text-yellow-500';
return 'text-red-500';
};
return (
<div className="bg-white/80 backdrop-blur-sm p-6 rounded-lg">
<div className="flex items-center gap-2 mb-4">
<Wifi className="h-5 w-5 text-cornflower-blue" />
<h2 className="text-xl font-semibold text-gray-900">Network Speed</h2>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className={`text-2xl font-bold ${getSpeedClass(speed)}`}>
{speed} Mbps
</span>
</div>
<p className="text-gray-600">{getRecommendation(speed)}</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { MapPin } from 'lucide-react';
import { type Mirror } from '@/types/download';
import { type UserLocation } from '@/lib/location';
interface SuggestedMirrorProps {
mirror: Mirror;
userLocation: UserLocation;
onSelect: (mirror: Mirror) => void;
}
export function SuggestedMirror({ mirror, userLocation, onSelect }: SuggestedMirrorProps) {
return (
<div className="bg-white/80 backdrop-blur-sm p-6 rounded-lg border-2 border-cornflower-blue">
<div className="flex items-center gap-2 mb-4">
<MapPin className="h-5 w-5 text-cornflower-blue" />
<div>
<h3 className="font-medium text-gray-900">Suggested Mirror</h3>
<p className="text-sm text-gray-500">Based on your location: {userLocation.city}, {userLocation.country}</p>
</div>
</div>
<div className="space-y-2">
<p className="font-medium text-gray-900">{mirror.name}</p>
<p className="text-sm text-gray-600">{mirror.location}</p>
<button
onClick={() => onSelect(mirror)}
className="w-full mt-2 px-4 py-2 bg-cornflower-blue text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Use This Mirror
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { Check } from 'lucide-react';
const requirements = [
'Minimum 2GB RAM (4GB recommended)',
'20GB disk space',
'CPU with virtualization support',
'DVD drive / USB boot support',
'Internet connectivity for updates',
];
export function SystemRequirements() {
return (
<div className="bg-white/80 backdrop-blur-sm p-6 rounded-lg">
<h2 className="text-xl font-semibold text-gray-900 mb-4">System Requirements</h2>
<ul className="space-y-3">
{requirements.map((req) => (
<li key={req} className="flex items-center gap-2">
<Check className="h-5 w-5 text-green-500 flex-shrink-0" />
<span className="text-gray-600">{req}</span>
</li>
))}
</ul>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { motion } from 'framer-motion';
interface CategoryFilterProps {
categories: string[];
selectedCategory: string;
onSelect: (category: string) => void;
}
export function CategoryFilter({ categories, selectedCategory, onSelect }: CategoryFilterProps) {
return (
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<motion.button
key={category}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onSelect(category)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category
? 'bg-cornflower-blue text-white'
: 'bg-white/80 text-gray-600 hover:bg-gray-100'
}`}
>
{category}
</motion.button>
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { Search } from 'lucide-react';
import { ChangeEvent } from 'react';
interface SearchBarProps {
value: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}
export function SearchBar({ value, onChange }: SearchBarProps) {
return (
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
value={value}
onChange={onChange}
placeholder="Search tools..."
className="w-full pl-10 pr-4 py-2 bg-white/80 backdrop-blur-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-cornflower-blue focus:border-transparent outline-none"
/>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { motion } from 'framer-motion';
import { Terminal } from 'lucide-react';
interface ToolCardProps {
name: string;
description: string;
category: string;
command: string;
}
export function ToolCard({ name, description, category, command }: ToolCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
whileHover={{ y: -5 }}
className="bg-white/80 backdrop-blur-sm p-6 rounded-lg shadow-sm border border-gray-100"
>
<div className="flex items-center gap-3 mb-3">
<Terminal className="h-5 w-5 text-cornflower-blue" />
<h3 className="text-lg font-semibold text-gray-900">{name}</h3>
</div>
<p className="text-gray-600 mb-4">{description}</p>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-cornflower-blue">{category}</span>
<code className="text-sm bg-gray-100 px-2 py-1 rounded">{command}</code>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,29 @@
import { motion } from 'framer-motion';
interface CategoryFilterProps {
categories: string[];
selectedCategory: string;
onSelect: (category: string) => void;
}
export function CategoryFilter({ categories, selectedCategory, onSelect }: CategoryFilterProps) {
return (
<div className="flex flex-wrap justify-center gap-3 mb-8">
{categories.map((category) => (
<motion.button
key={category}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onSelect(category)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedCategory === category
? 'bg-cornflower-blue text-white'
: 'bg-white text-gray-600 hover:bg-gray-100'
}`}
>
{category}
</motion.button>
))}
</div>
);
}

View File

@@ -0,0 +1,30 @@
import { motion } from 'framer-motion';
interface GalleryImageProps {
src: string;
alt: string;
category: string;
}
export function GalleryImage({ src, alt, category }: GalleryImageProps) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
whileHover={{ y: -5 }}
className="relative group overflow-hidden rounded-lg"
>
<img
src={src}
alt={alt}
className="w-full h-64 object-cover transform group-hover:scale-110 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-0 left-0 right-0 p-4">
<p className="text-white font-medium">{alt}</p>
<span className="text-sm text-gray-300">{category}</span>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,60 @@
import { motion } from 'framer-motion';
import { Check, X } from 'lucide-react';
const features = [
'Advanced Security Tools',
'Regular Updates',
'Community Support',
'Hardware Compatibility',
'Custom Tools',
'Enterprise Support',
];
export function ComparisonSection() {
return (
<section className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900">Why Choose Snigdha OS?</h2>
<p className="mt-4 text-lg text-gray-600">
Compare and see why security professionals prefer Snigdha OS
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{['Other Distros', 'Snigdha OS', 'Commercial Tools'].map((title, colIndex) => (
<motion.div
key={title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: colIndex * 0.2 }}
className={`bg-white rounded-lg shadow-lg overflow-hidden ${
colIndex === 1 ? 'ring-2 ring-cornflower-blue' : ''
}`}
>
<div className={`p-6 ${
colIndex === 1 ? 'bg-cornflower-blue text-white' : 'bg-gray-50'
}`}>
<h3 className="text-xl font-semibold">{title}</h3>
</div>
<div className="p-6">
<ul className="space-y-4">
{features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
{colIndex === 1 ? (
<Check className="h-5 w-5 text-green-500" />
) : (
<X className="h-5 w-5 text-red-500" />
)}
<span className="text-gray-600">{feature}</span>
</li>
))}
</ul>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,30 @@
import { motion } from 'framer-motion';
import { LucideIcon } from 'lucide-react';
interface FeatureCardProps {
title: string;
description: string;
icon: LucideIcon;
delay?: number;
}
export function FeatureCard({ title, description, icon: Icon, delay = 0 }: FeatureCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay }}
viewport={{ once: true }}
whileHover={{ y: -5 }}
className="relative group"
>
<div className="rounded-xl bg-white/80 backdrop-blur p-8 ring-1 ring-gray-200 hover:ring-cornflower-blue transition-all shadow-lg overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-cornflower-blue/0 to-cornflower-blue/0 group-hover:from-cornflower-blue/5 group-hover:to-cornflower-blue/10 transition-colors" />
<Icon className="h-10 w-10 text-cornflower-blue mb-4" />
<h3 className="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-gray-600">{description}</p>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,76 @@
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { Download, ChevronRight, Terminal } from 'lucide-react';
export function HeroSection() {
return (
<section className="relative min-h-[90vh] flex items-center overflow-hidden bg-gradient-to-r from-gray-900 to-gray-800">
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1629654297299-c8506221ca97?auto=format&fit=crop&q=80')] bg-cover bg-center opacity-10" />
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-gray-900/50" />
<div className="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="text-center"
>
<motion.div
animate={{
rotate: [0, 5, -5, 0],
scale: [1, 1.1, 1]
}}
transition={{ duration: 2, repeat: Infinity, repeatDelay: 3 }}
>
<Terminal className="mx-auto h-20 w-20 text-cornflower-blue" />
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mt-6 text-4xl font-bold tracking-tight text-white sm:text-6xl"
>
The Future of
<span className="text-cornflower-blue"> Security Testing </span>
is Here
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mt-6 text-lg leading-8 text-gray-300 max-w-2xl mx-auto"
>
Snigdha OS redefines penetration testing with advanced tools, intuitive interface, and unmatched performance.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.9 }}
className="mt-10 flex items-center justify-center gap-x-6"
>
<Link
to="/download"
className="group relative rounded-lg bg-cornflower-blue px-8 py-3 text-sm font-semibold text-white shadow-lg hover:bg-blue-600 transition-colors overflow-hidden"
>
<span className="relative flex items-center gap-2">
<Download className="h-4 w-4" />
Download Now
</span>
<div className="absolute inset-0 bg-white/20 transform -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-700" />
</Link>
<Link
to="/features"
className="text-sm font-semibold leading-6 text-white flex items-center group"
>
Learn more
<ChevronRight className="ml-1 h-4 w-4 transform group-hover:translate-x-1 transition-transform" />
</Link>
</motion.div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,41 @@
import { motion } from 'framer-motion';
import { Users, Wrench, Star } from 'lucide-react';
const stats = [
{ label: 'Active Users', value: '50K+', icon: Users },
{ label: 'Security Tools', value: '600+', icon: Wrench },
{ label: 'GitHub Stars', value: '15K+', icon: Star },
];
export function StatsSection() {
return (
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{stats.map((stat, index) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.2 }}
viewport={{ once: true }}
className="text-center"
>
<stat.icon className="h-8 w-8 text-cornflower-blue mx-auto mb-4" />
<motion.p
initial={{ scale: 0.5 }}
whileInView={{ scale: 1 }}
transition={{ delay: index * 0.2 + 0.2 }}
viewport={{ once: true }}
className="text-4xl font-bold text-gray-900 mb-2"
>
{stat.value}
</motion.p>
<p className="text-gray-600">{stat.label}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,27 @@
import { motion } from 'framer-motion';
import { Quote } from 'lucide-react';
interface TestimonialProps {
content: string;
author: string;
role: string;
delay?: number;
}
export function TestimonialCard({ content, author, role, delay = 0 }: TestimonialProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay }}
className="bg-white/80 backdrop-blur-sm p-6 rounded-lg shadow-lg relative"
>
<Quote className="absolute top-4 right-4 h-8 w-8 text-cornflower-blue/20" />
<p className="text-gray-600 mb-4">{content}</p>
<div>
<p className="font-semibold text-gray-900">{author}</p>
<p className="text-sm text-gray-500">{role}</p>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,41 @@
import { motion } from 'framer-motion';
import { Terminal, Shield, Wifi, Globe, Lock, Database } from 'lucide-react';
const tools = [
{ name: 'Network Analysis', icon: Globe, color: 'text-blue-500' },
{ name: 'Penetration Testing', icon: Shield, color: 'text-green-500' },
{ name: 'Wireless Security', icon: Wifi, color: 'text-purple-500' },
{ name: 'Cryptography', icon: Lock, color: 'text-red-500' },
{ name: 'Forensics', icon: Database, color: 'text-yellow-500' },
{ name: 'Exploitation', icon: Terminal, color: 'text-pink-500' },
];
export function ToolsShowcase() {
return (
<section className="py-20 bg-gradient-to-b from-gray-50 to-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900">Security Tools Suite</h2>
<p className="mt-4 text-lg text-gray-600">
Comprehensive toolkit for security professionals
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
{tools.map((tool, index) => (
<motion.div
key={tool.name}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="flex flex-col items-center p-6 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
<tool.icon className={`h-10 w-10 ${tool.color} mb-4`} />
<h3 className="text-lg font-semibold text-gray-900">{tool.name}</h3>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,103 @@
import { Link } from 'react-router-dom';
import { Github, Twitter, Youtube, Mail, Book, FileText, MessageSquare, Newspaper, HelpCircle } from 'lucide-react';
const footerNavigation = {
main: [
{ name: 'Gallery', href: '/gallery' },
{ name: 'Developers', href: '/developers' },
{ name: 'Donate', href: '/donate' },
],
resources: [
{ name: 'Documentation', href: '/docs', icon: Book },
{ name: 'Blog', href: '/blog', icon: Newspaper },
{ name: 'Support', href: '/support', icon: HelpCircle },
],
community: [
{ name: 'Community', href: '/community', icon: MessageSquare },
{ name: 'GitHub', href: 'https://github.com/Snigdha-OS', icon: Github },
{ name: 'Twitter', href: 'https://twitter.com', icon: Twitter },
{ name: 'YouTube', href: 'https://youtube.com', icon: Youtube },
],
};
export function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-gray-900 text-gray-300">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Snigdha OS</h3>
<p className="text-sm">
The most advanced penetration testing distribution, designed for security professionals and enthusiasts.
</p>
<div className="flex space-x-4">
{footerNavigation.community.map((item) => (
<a
key={item.name}
href={item.href}
className="hover:text-white transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<item.icon className="h-5 w-5" />
</a>
))}
</div>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">Navigation</h3>
<ul className="space-y-3">
{footerNavigation.main.map((item) => (
<li key={item.name}>
<Link to={item.href} className="hover:text-white transition-colors">
{item.name}
</Link>
</li>
))}
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">Resources</h3>
<ul className="space-y-3">
{footerNavigation.resources.map((item) => (
<li key={item.name}>
<Link to={item.href} className="flex items-center gap-2 hover:text-white transition-colors">
<item.icon className="h-4 w-4" />
{item.name}
</Link>
</li>
))}
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">Community</h3>
<ul className="space-y-3">
{footerNavigation.community.map((item) => (
<li key={item.name}>
<a
href={item.href}
className="flex items-center gap-2 hover:text-white transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<item.icon className="h-4 w-4" />
{item.name}
</a>
</li>
))}
</ul>
</div>
</div>
<div className="mt-12 pt-8 border-t border-gray-800 text-sm text-center">
<p>&copy; {currentYear} Snigdha OS. All rights reserved.</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,28 @@
import { Mail, MapPin, Phone } from 'lucide-react';
export function ContactSection() {
return (
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">Contact Us</h3>
<ul className="space-y-3">
<li>
<a
href="mailto:contact@snigdhaos.org"
className="flex items-center gap-2 hover:text-white transition-colors"
>
<Mail className="h-4 w-4" />
contact@snigdhaos.org
</a>
</li>
<li className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
<span>Bangalore, India</span>
</li>
<li className="flex items-center gap-2">
<Phone className="h-4 w-4" />
<span>+91 (080) 4567-8900</span>
</li>
</ul>
</div>
);
}

View File

@@ -0,0 +1,75 @@
import { Link } from 'react-router-dom';
import { Github, Twitter, Youtube, Book, MessageSquare, Newspaper, HelpCircle } from 'lucide-react';
import { ContactSection } from './ContactSection';
import { footerNavigation } from '@/data/footerNavigation';
export function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-gray-900 text-gray-300">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-5 gap-8">
<div className="space-y-4 md:col-span-2">
<h3 className="text-lg font-semibold text-white">Snigdha OS</h3>
<p className="text-sm">
The most advanced penetration testing distribution, designed for security professionals and enthusiasts.
</p>
<div className="flex space-x-4">
{footerNavigation.community.map((item) => (
<a
key={item.name}
href={item.href}
className="hover:text-white transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<item.icon className="h-5 w-5" />
</a>
))}
</div>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">Resources</h3>
<ul className="space-y-3">
{footerNavigation.resources.map((item) => (
<li key={item.name}>
<Link to={item.href} className="flex items-center gap-2 hover:text-white transition-colors">
<item.icon className="h-4 w-4" />
{item.name}
</Link>
</li>
))}
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider mb-4">Community</h3>
<ul className="space-y-3">
{footerNavigation.community.map((item) => (
<li key={item.name}>
<a
href={item.href}
className="flex items-center gap-2 hover:text-white transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<item.icon className="h-4 w-4" />
{item.name}
</a>
</li>
))}
</ul>
</div>
<ContactSection />
</div>
<div className="mt-12 pt-8 border-t border-gray-800 text-sm text-center">
<p>&copy; {currentYear} Snigdha OS. All rights reserved.</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,84 @@
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Menu, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { navigation } from '@/data/navigation';
function Logo() {
return (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 2L3 9V23L16 30L29 23V9L16 2Z" stroke="#6495ED" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 2V16M16 16V30M16 16L29 9M16 16L3 9" stroke="#6495ED" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
}
export function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const location = useLocation();
return (
<nav className="fixed w-full z-50 bg-white/80 backdrop-blur-lg border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<Link to="/" className="flex-shrink-0 flex items-center">
<Logo />
<span className="ml-2 text-xl font-bold text-gray-900">Snigdha OS</span>
</Link>
</div>
<div className="hidden sm:flex sm:items-center sm:space-x-8">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className={cn(
'px-3 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-2',
location.pathname === item.href
? 'text-cornflower-blue'
: 'text-gray-600 hover:text-cornflower-blue'
)}
>
<item.icon className="h-4 w-4" />
{item.name}
</Link>
))}
</div>
<div className="flex items-center sm:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100"
>
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
</div>
</div>
</div>
{isOpen && (
<div className="sm:hidden bg-white/90 backdrop-blur-lg">
<div className="px-2 pt-2 pb-3 space-y-1">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className={cn(
'block px-3 py-2 rounded-md text-base font-medium flex items-center gap-2',
location.pathname === item.href
? 'text-cornflower-blue bg-blue-50'
: 'text-gray-600 hover:text-cornflower-blue hover:bg-blue-50'
)}
onClick={() => setIsOpen(false)}
>
<item.icon className="h-4 w-4" />
{item.name}
</Link>
))}
</div>
</div>
)}
</nav>
);
}

View File

@@ -0,0 +1,34 @@
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import { AlertCircle } from 'lucide-react';
function ErrorFallback({ error }: { error: Error }) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full p-6 bg-white/80 backdrop-blur-lg rounded-lg shadow-lg">
<div className="flex items-center justify-center text-red-500 mb-4">
<AlertCircle size={48} />
</div>
<h2 className="text-2xl font-bold text-gray-900 text-center mb-4">
Something went wrong
</h2>
<pre className="text-sm bg-gray-100 p-4 rounded overflow-auto">
{error.message}
</pre>
<button
onClick={() => window.location.reload()}
className="mt-4 w-full bg-cornflower-blue text-white py-2 px-4 rounded hover:bg-blue-600 transition-colors"
>
Try again
</button>
</div>
</div>
);
}
export function ErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ReactErrorBoundary FallbackComponent={ErrorFallback}>
{children}
</ReactErrorBoundary>
);
}