geocities

This commit is contained in:
2026-01-21 22:00:00 +00:00
parent efffbe2c0a
commit e70fe69041
73 changed files with 3849 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
# ~*~RiChArDs HoMePaGe~*~
> A modern React/Next.js application that looks like it escaped from 1996
## 🔥 Features
- **Modern Architecture**: Next.js 14, TypeScript, SASS modules
- **Atomic Design**: Proper component hierarchy (atoms → molecules → organisms → templates)
- **Custom Hooks**: `useIndexedDB`, `useVisitorCounter`, `useGuestbook`
- **IndexedDB Persistence**: Visitor counter and guestbook survive page reloads
- **90s Aesthetic**: All the GeoCities hallmarks you remember (or want to forget)
## 🚀 Tech Stack
- **Framework**: Next.js 14 (App Router)
- **Language**: TypeScript
- **Styling**: SASS/SCSS Modules
- **State Management**: React hooks + IndexedDB
- **Architecture**: Atomic Design Pattern
## 📁 Project Structure
```
geocities-app/
├── app/
│ ├── layout.tsx # Root layout with fonts
│ └── page.tsx # Home page with splash/main routing
├── components/
│ ├── atoms/ # Basic building blocks
│ │ ├── BlinkText/ # <blink> tag energy
│ │ ├── MarqueeText/ # Scrolling text
│ │ ├── RetroGif/ # Classic animated gifs
│ │ ├── RetroButton/ # Windows 95 style buttons
│ │ ├── RetroInput/ # Inset text inputs
│ │ ├── RetroTextarea/ # Inset textareas
│ │ └── RainbowText/ # Animated rainbow text
│ ├── molecules/ # Combined atoms
│ │ ├── VisitorCounter/ # Hit counter display
│ │ ├── GuestbookForm/ # Sign the guestbook
│ │ ├── GuestbookEntry/ # Individual guestbook entry
│ │ ├── NavLink/ # Navigation links with badges
│ │ └── WebRing/ # Remember web rings?
│ ├── organisms/ # Complex components
│ │ ├── Header/ # Site header with marquee
│ │ ├── Sidebar/ # Navigation, counter, awards
│ │ ├── MainContent/ # About, links, updates
│ │ ├── Guestbook/ # Full guestbook section
│ │ └── Footer/ # Credits and badges
│ └── templates/ # Page layouts
│ ├── SplashScreen/ # ENTER splash page
│ └── MainLayout/ # Main site layout
├── hooks/
│ ├── useIndexedDB.ts # Generic IndexedDB hook
│ ├── useVisitorCounter.ts # Visitor counter state
│ └── useGuestbook.ts # Guestbook CRUD
└── styles/
└── globals.scss # Global 90s styles
```
## 🏃 Running Locally
```bash
npm install
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) to enter the time warp.
## ✨ GeoCities Elements Included
- [x] Cheesy "ENTER" splash screen
- [x] Animated cursor trail (sparkles!)
- [x] Blinking text
- [x] Rainbow text
- [x] Marquee scrolling
- [x] Under construction GIFs
- [x] Dancing baby GIF
- [x] Fire GIFs
- [x] Skull GIF
- [x] Visitor counter
- [x] Guestbook
- [x] Web ring
- [x] "Best viewed with Netscape/IE" badges
- [x] Award badges
- [x] Windows 95 style buttons
- [x] Ridge/groove borders everywhere
- [x] Tiled backgrounds
- [x] Comic Sans (via Comic Neue)
- [x] wEiRd CaPiTaLiZaTiOn
## 🎨 Design Philosophy
**The Ironic Stack**: Maximum modern engineering practices wrapped in maximum 90s web chaos. TypeScript strict mode enforcing the type safety of your `<blink>` tag replacement.
Built with genuine affection for the web's weird, wonderful early days.
---
*MaDe WiTh 💕 aNd Notepad.exe*
*(AcTuAlLy MaDe WiTh Next.js, React, TypeScript, SASS & IndexedDB)*

View File

@@ -0,0 +1,29 @@
import type { Metadata } from 'next';
import '@/styles/globals.scss';
export const metadata: Metadata = {
title: '~*~RiChArDs HoMePaGe~*~',
description: 'WeLcOmE tO mY pErSoNaL wEbSiTe!!!1!',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Comic+Neue:wght@400;700&family=VT323&family=Press+Start+2P&display=swap"
rel="stylesheet"
/>
</head>
<body>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,43 @@
'use client';
import { useState, useEffect } from 'react';
import { SplashScreen } from '@/components/templates/SplashScreen';
import { MainLayout } from '@/components/templates/MainLayout';
import { useVisitorCounter } from '@/hooks/useVisitorCounter';
import { useGuestbook } from '@/hooks/useGuestbook';
export default function Home() {
const [hasEntered, setHasEntered] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { visitorCount, incrementVisitor } = useVisitorCounter();
const { entries, addEntry, isLoaded } = useGuestbook();
useEffect(() => {
const entered = sessionStorage.getItem('hasEntered');
if (entered === 'true') {
setHasEntered(true);
}
setIsLoading(false);
}, []);
const handleEnter = async () => {
await incrementVisitor();
sessionStorage.setItem('hasEntered', 'true');
setHasEntered(true);
};
if (isLoading) return null;
if (!hasEntered) {
return <SplashScreen onEnter={handleEnter} visitorCount={visitorCount} />;
}
return (
<MainLayout
visitorCount={visitorCount}
guestbookEntries={entries}
onGuestbookSubmit={addEntry}
isGuestbookLoaded={isLoaded}
/>
);
}

View File

@@ -0,0 +1,26 @@
.blink {
animation-name: blink;
animation-iteration-count: infinite;
font-weight: bold;
&.slow {
animation-duration: 1.5s;
}
&.normal {
animation-duration: 1s;
}
&.fast {
animation-duration: 0.5s;
}
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0;
}
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import styles from './BlinkText.module.scss';
interface BlinkTextProps {
children: React.ReactNode;
color?: string;
speed?: 'slow' | 'normal' | 'fast';
className?: string;
}
export const BlinkText: React.FC<BlinkTextProps> = ({
children,
color = '#ff0000',
speed = 'normal',
className = ''
}) => {
return (
<span
className={`${styles.blink} ${styles[speed]} ${className}`}
style={{ color }}
>
{children}
</span>
);
};

View File

@@ -0,0 +1 @@
export { BlinkText } from './BlinkText';

View File

@@ -0,0 +1,61 @@
.marqueeContainer {
width: 100%;
overflow: hidden;
background: linear-gradient(90deg, #000080, #0000aa, #000080);
padding: 8px 0;
border-top: 3px ridge #c0c0c0;
border-bottom: 3px ridge #c0c0c0;
}
.marquee {
display: flex;
width: max-content;
span {
padding: 0 50px;
white-space: nowrap;
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1.2rem;
font-weight: bold;
color: #ffff00;
text-shadow: 2px 2px #ff0000;
}
&.left {
animation: scrollLeft var(--duration) linear infinite;
}
&.right {
animation: scrollRight var(--duration) linear infinite;
}
&.slow {
--duration: 30s;
}
&.normal {
--duration: 15s;
}
&.fast {
--duration: 8s;
}
}
@keyframes scrollLeft {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
@keyframes scrollRight {
0% {
transform: translateX(-50%);
}
100% {
transform: translateX(0);
}
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import styles from './MarqueeText.module.scss';
interface MarqueeTextProps {
children: React.ReactNode;
direction?: 'left' | 'right';
speed?: 'slow' | 'normal' | 'fast';
className?: string;
}
export const MarqueeText: React.FC<MarqueeTextProps> = ({
children,
direction = 'left',
speed = 'normal',
className = ''
}) => {
return (
<div className={`${styles.marqueeContainer} ${className}`}>
<div className={`${styles.marquee} ${styles[direction]} ${styles[speed]}`}>
<span>{children}</span>
<span>{children}</span>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { MarqueeText } from './MarqueeText';

View File

@@ -0,0 +1,27 @@
.rainbowAnimated {
background: linear-gradient(
90deg,
#ff0000,
#ff7f00,
#ffff00,
#00ff00,
#0000ff,
#4b0082,
#9400d3,
#ff0000
);
background-size: 200% auto;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: rainbow 3s linear infinite;
}
@keyframes rainbow {
0% {
background-position: 0% center;
}
100% {
background-position: 200% center;
}
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import styles from './RainbowText.module.scss';
interface RainbowTextProps {
children: string;
animated?: boolean;
className?: string;
}
export const RainbowText: React.FC<RainbowTextProps> = ({
children,
animated = true,
className = ''
}) => {
if (animated) {
return (
<span className={`${styles.rainbowAnimated} ${className}`}>
{children}
</span>
);
}
// Static rainbow - each letter gets a color
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
return (
<span className={className}>
{children.split('').map((char, i) => (
<span key={i} style={{ color: colors[i % colors.length] }}>
{char}
</span>
))}
</span>
);
};

View File

@@ -0,0 +1 @@
export { RainbowText } from './RainbowText';

View File

@@ -0,0 +1,67 @@
.retroButton {
font-family: 'VT323', 'MS Sans Serif', monospace;
cursor: pointer;
background: #c0c0c0;
border: none;
padding: 4px 16px;
position: relative;
// Classic Windows 95/98 3D border effect
box-shadow:
inset -1px -1px #0a0a0a,
inset 1px 1px #ffffff,
inset -2px -2px #808080,
inset 2px 2px #dfdfdf;
&:active:not(:disabled) {
box-shadow:
inset 1px 1px #0a0a0a,
inset -1px -1px #ffffff,
inset 2px 2px #808080,
inset -2px -2px #dfdfdf;
padding: 5px 15px 3px 17px;
}
&:focus {
outline: 1px dotted #000;
outline-offset: -4px;
}
&:disabled {
color: #808080;
cursor: not-allowed;
}
// Sizes
&.small {
font-size: 14px;
padding: 2px 8px;
}
&.medium {
font-size: 18px;
padding: 4px 16px;
}
&.large {
font-size: 24px;
padding: 8px 32px;
}
// Variants
&.primary {
background: linear-gradient(180deg, #c0c0c0 0%, #a0a0a0 100%);
color: #000;
}
&.secondary {
background: linear-gradient(180deg, #d4d0c8 0%, #b4b0a8 100%);
color: #000080;
}
&.danger {
background: linear-gradient(180deg, #ff6b6b 0%, #cc0000 100%);
color: #fff;
text-shadow: 1px 1px 0 #800000;
}
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import styles from './RetroButton.module.scss';
interface RetroButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
className?: string;
}
export const RetroButton: React.FC<RetroButtonProps> = ({
children,
onClick,
variant = 'primary',
size = 'medium',
disabled = false,
type = 'button',
className = ''
}) => {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
className={`${styles.retroButton} ${styles[variant]} ${styles[size]} ${className}`}
>
{children}
</button>
);
};

View File

@@ -0,0 +1 @@
export { RetroButton } from './RetroButton';

View File

@@ -0,0 +1,19 @@
.retroGif {
image-rendering: pixelated;
vertical-align: middle;
&.small {
height: 20px;
width: auto;
}
&.medium {
height: 40px;
width: auto;
}
&.large {
height: 80px;
width: auto;
}
}

View File

@@ -0,0 +1,84 @@
import React from 'react';
import styles from './RetroGif.module.scss';
// Using archive.org hosted classic 90s gifs
const GIF_SOURCES = {
underConstruction: 'https://web.archive.org/web/20091027010629im_/http://geocities.com/js_source/uc.gif',
underConstruction2: 'https://web.archive.org/web/20091027010629im_/http://geocities.com/SiliconValley/Park/6543/underconstruction.gif',
email: 'https://web.archive.org/web/20091027065743im_/http://www.geocities.com/SunsetStrip/Venue/8232/emailme2.gif',
fire: 'https://web.archive.org/web/20091027003401im_/http://geocities.com/js_source/fire.gif',
new: 'https://web.archive.org/web/20091027010844im_/http://geocities.com/ResearchTriangle/Forum/1363/new.gif',
counter: 'https://web.archive.org/web/20091027012438im_/http://www.geocities.com/Heartland/Plains/7646/counter.gif',
welcome: 'https://web.archive.org/web/20091027003221im_/http://geocities.com/SiliconValley/Way/4302/welcome5.gif',
dividerRainbow: 'https://web.archive.org/web/20091027010629im_/http://geocities.com/EnchantedForest/Glade/1274/rainbow.gif',
guestbook: 'https://web.archive.org/web/20091027010348im_/http://geocities.com/EnchantedForest/Dell/4250/guestbook.gif',
dancing: 'https://web.archive.org/web/20091027004205im_/http://geocities.com/SiliconValley/Hills/5765/babydan.gif',
skull: 'https://web.archive.org/web/20091027002812im_/http://geocities.com/Area51/Zone/7492/skullani.gif',
spinning3d: 'https://web.archive.org/web/20091027012156im_/http://geocities.com/ResearchTriangle/6640/at3.gif',
coolSite: 'https://web.archive.org/web/20091027003506im_/http://geocities.com/SunsetStrip/Palladium/9782/cool.gif',
netscape: 'https://web.archive.org/web/20091027001652im_/http://www.geocities.com/SiliconValley/Platform/4483/netscape.gif',
ielogo: 'https://web.archive.org/web/20091027011037im_/http://geocities.com/SiliconValley/Pines/6818/msielogo.gif',
hotSite: 'https://web.archive.org/web/20091026235944im_/http://geocities.com/Heartland/Hills/9498/hot.gif',
} as const;
// Fallback inline SVGs for when archive.org is slow/unavailable
const FALLBACK_SVGS: Record<string, string> = {
underConstruction: `data:image/svg+xml,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 88 31">
<rect fill="#ff0" width="88" height="31"/>
<text x="44" y="20" text-anchor="middle" font-family="Arial" font-size="10" fill="#000">🚧 UNDER</text>
<text x="44" y="28" text-anchor="middle" font-family="Arial" font-size="8" fill="#000">CONSTRUCTION</text>
</svg>
`)}`,
fire: `data:image/svg+xml,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 30">
<text x="10" y="20" text-anchor="middle" font-size="20">🔥</text>
</svg>
`)}`,
email: `data:image/svg+xml,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30">
<text x="30" y="20" text-anchor="middle" font-size="16">📧 EMAIL</text>
</svg>
`)}`,
default: `data:image/svg+xml,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<rect fill="#c0c0c0" width="40" height="40" stroke="#808080"/>
<text x="20" y="25" text-anchor="middle" font-size="20">?</text>
</svg>
`)}`,
};
type GifType = keyof typeof GIF_SOURCES;
interface RetroGifProps {
type: GifType;
alt?: string;
className?: string;
size?: 'small' | 'medium' | 'large';
}
export const RetroGif: React.FC<RetroGifProps> = ({
type,
alt,
className = '',
size = 'medium'
}) => {
const src = GIF_SOURCES[type];
const fallback = FALLBACK_SVGS[type] || FALLBACK_SVGS.default;
const altText = alt || type.replace(/([A-Z])/g, ' $1').trim();
return (
<img
src={src}
alt={altText}
className={`${styles.retroGif} ${styles[size]} ${className}`}
onError={(e) => {
(e.target as HTMLImageElement).src = fallback;
}}
loading="lazy"
/>
);
};
export { GIF_SOURCES };
export type { GifType };

View File

@@ -0,0 +1,2 @@
export { RetroGif, GIF_SOURCES } from './RetroGif';
export type { GifType } from './RetroGif';

View File

@@ -0,0 +1,24 @@
.retroInput {
font-family: 'VT323', 'Courier New', monospace;
font-size: 16px;
padding: 4px 8px;
background: #fff;
border: none;
// Inset 3D border effect (opposite of button)
box-shadow:
inset 1px 1px #0a0a0a,
inset -1px -1px #ffffff,
inset 2px 2px #808080,
inset -2px -2px #dfdfdf;
&:focus {
outline: none;
background: #ffffcc;
}
&::placeholder {
color: #808080;
font-style: italic;
}
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import styles from './RetroInput.module.scss';
interface RetroInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
type?: 'text' | 'email' | 'password';
name?: string;
required?: boolean;
className?: string;
}
export const RetroInput: React.FC<RetroInputProps> = ({
value,
onChange,
placeholder,
type = 'text',
name,
required = false,
className = ''
}) => {
return (
<input
type={type}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
className={`${styles.retroInput} ${className}`}
/>
);
};

View File

@@ -0,0 +1 @@
export { RetroInput } from './RetroInput';

View File

@@ -0,0 +1,25 @@
.retroTextarea {
font-family: 'VT323', 'Courier New', monospace;
font-size: 16px;
padding: 8px;
background: #fff;
border: none;
resize: vertical;
width: 100%;
box-shadow:
inset 1px 1px #0a0a0a,
inset -1px -1px #ffffff,
inset 2px 2px #808080,
inset -2px -2px #dfdfdf;
&:focus {
outline: none;
background: #ffffcc;
}
&::placeholder {
color: #808080;
font-style: italic;
}
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import styles from './RetroTextarea.module.scss';
interface RetroTextareaProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
name?: string;
required?: boolean;
rows?: number;
className?: string;
}
export const RetroTextarea: React.FC<RetroTextareaProps> = ({
value,
onChange,
placeholder,
name,
required = false,
rows = 4,
className = ''
}) => {
return (
<textarea
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
rows={rows}
className={`${styles.retroTextarea} ${className}`}
/>
);
};

View File

@@ -0,0 +1 @@
export { RetroTextarea } from './RetroTextarea';

View File

@@ -0,0 +1,8 @@
export { BlinkText } from './BlinkText';
export { MarqueeText } from './MarqueeText';
export { RetroGif, GIF_SOURCES } from './RetroGif';
export type { GifType } from './RetroGif';
export { RetroButton } from './RetroButton';
export { RetroInput } from './RetroInput';
export { RetroTextarea } from './RetroTextarea';
export { RainbowText } from './RainbowText';

View File

@@ -0,0 +1,52 @@
.entry {
background: #ffffcc;
border: 2px solid #808000;
padding: 10px;
margin-bottom: 10px;
&:nth-child(even) {
background: #ccffcc;
border-color: #008000;
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 5px;
border-bottom: 1px dashed #808080;
}
.name {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-weight: bold;
font-size: 1rem;
color: #000080;
}
.date {
font-family: 'VT323', monospace;
font-size: 0.9rem;
color: #808080;
}
.message {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 0.95rem;
color: #333;
line-height: 1.4;
margin-bottom: 5px;
}
.email {
font-family: 'VT323', monospace;
font-size: 0.85rem;
color: #0000ff;
text-decoration: underline;
&:hover {
color: #ff00ff;
}
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import type { GuestbookEntry as GuestbookEntryType } from '@/hooks/useGuestbook';
import styles from './GuestbookEntry.module.scss';
interface GuestbookEntryProps {
entry: GuestbookEntryType;
}
export const GuestbookEntry: React.FC<GuestbookEntryProps> = ({ entry }) => {
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
return (
<div className={styles.entry}>
<div className={styles.header}>
<span className={styles.name}>{entry.name}</span>
<span className={styles.date}>{formatDate(entry.date)}</span>
</div>
<div className={styles.message}>{entry.message}</div>
{entry.email && (
<a href={`mailto:${entry.email}`} className={styles.email}>
📧 {entry.email}
</a>
)}
</div>
);
};

View File

@@ -0,0 +1 @@
export { GuestbookEntry } from './GuestbookEntry';

View File

@@ -0,0 +1,59 @@
.formContainer {
background: #c0c0c0;
border: 3px outset #dfdfdf;
padding: 15px;
margin: 20px 0;
}
.title {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1.2rem;
color: #800080;
text-align: center;
margin-bottom: 15px;
text-shadow: 1px 1px 0 #fff;
}
.form {
display: flex;
flex-direction: column;
gap: 12px;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 14px;
color: #000080;
font-weight: bold;
}
}
.buttonRow {
display: flex;
justify-content: center;
margin-top: 10px;
}
.thankYou {
text-align: center;
padding: 20px;
background: #ffffcc;
border: 2px dashed #ff00ff;
p {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1.1rem;
color: #008000;
margin: 5px 0;
&:first-child {
font-size: 1.3rem;
font-weight: bold;
}
}
}

View File

@@ -0,0 +1,84 @@
'use client';
import React, { useState } from 'react';
import { RetroButton } from '@/components/atoms/RetroButton';
import { RetroInput } from '@/components/atoms/RetroInput';
import { RetroTextarea } from '@/components/atoms/RetroTextarea';
import styles from './GuestbookForm.module.scss';
interface GuestbookFormProps {
onSubmit: (name: string, message: string, email?: string) => void;
}
export const GuestbookForm: React.FC<GuestbookFormProps> = ({ onSubmit }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name && message) {
onSubmit(name, message, email || undefined);
setName('');
setEmail('');
setMessage('');
setSubmitted(true);
setTimeout(() => setSubmitted(false), 3000);
}
};
return (
<div className={styles.formContainer}>
<h3 className={styles.title}>
SiGn My GuEsTbOoK!
</h3>
{submitted ? (
<div className={styles.thankYou}>
<p>🎉 ThAnK yOu FoR sIgNiNg!!! 🎉</p>
<p>YoUr EnTrY hAs BeEn AdDeD!</p>
</div>
) : (
<div className={styles.form}>
<div className={styles.field}>
<label>YoUr NaMe:</label>
<RetroInput
value={name}
onChange={setName}
placeholder="Enter your name..."
required
/>
</div>
<div className={styles.field}>
<label>E-mAiL (optional):</label>
<RetroInput
value={email}
onChange={setEmail}
placeholder="your@email.com"
type="email"
/>
</div>
<div className={styles.field}>
<label>YoUr MeSsAgE:</label>
<RetroTextarea
value={message}
onChange={setMessage}
placeholder="Leave a message..."
rows={4}
required
/>
</div>
<div className={styles.buttonRow}>
<RetroButton onClick={handleSubmit} variant="primary" size="large">
📝 SuBmIt 📝
</RetroButton>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1 @@
export { GuestbookForm } from './GuestbookForm';

View File

@@ -0,0 +1,58 @@
.navLink {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
color: #0000ff;
text-decoration: none;
background: transparent;
border: 1px solid transparent;
transition: all 0.1s;
&:hover {
color: #ff00ff;
background: #ffffcc;
border: 1px dashed #ff00ff;
text-decoration: underline;
}
&:visited {
color: #800080;
}
}
.icon {
font-size: 1.2em;
}
.text {
text-decoration: underline;
}
.badge {
font-family: 'Press Start 2P', monospace;
font-size: 0.5rem;
padding: 2px 4px;
animation: pulse 0.5s infinite;
&.new {
background: #00ff00;
color: #000;
}
&.hot {
background: #ff0000;
color: #fff;
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import styles from './NavLink.module.scss';
interface NavLinkProps {
href: string;
children: React.ReactNode;
icon?: string;
isNew?: boolean;
isHot?: boolean;
onClick?: () => void;
}
export const NavLink: React.FC<NavLinkProps> = ({
href,
children,
icon,
isNew = false,
isHot = false,
onClick
}) => {
const handleClick = (e: React.MouseEvent) => {
if (href.startsWith('#')) {
e.preventDefault();
const element = document.querySelector(href);
element?.scrollIntoView({ behavior: 'smooth' });
}
onClick?.();
};
return (
<a href={href} onClick={handleClick} className={styles.navLink}>
{icon && <span className={styles.icon}>{icon}</span>}
<span className={styles.text}>{children}</span>
{isNew && <span className={styles.badge + ' ' + styles.new}>NEW!</span>}
{isHot && <span className={styles.badge + ' ' + styles.hot}>HOT!</span>}
</a>
);
};

View File

@@ -0,0 +1 @@
export { NavLink } from './NavLink';

View File

@@ -0,0 +1,44 @@
.counterContainer {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
background: #000;
border: 3px ridge #808080;
}
.label {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 12px;
color: #00ff00;
margin-bottom: 5px;
text-transform: uppercase;
}
.counter {
display: flex;
gap: 2px;
background: #111;
padding: 5px;
border: 2px inset #333;
}
.digit {
font-family: 'VT323', 'Courier New', monospace;
font-size: 24px;
color: #ff0000;
background: #1a0000;
padding: 2px 6px;
border: 1px solid #330000;
text-shadow: 0 0 5px #ff0000;
min-width: 18px;
text-align: center;
}
.since {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 10px;
color: #808080;
margin-top: 5px;
font-style: italic;
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import styles from './VisitorCounter.module.scss';
interface VisitorCounterProps {
count: number;
className?: string;
}
export const VisitorCounter: React.FC<VisitorCounterProps> = ({
count,
className = ''
}) => {
const paddedCount = count.toString().padStart(7, '0');
return (
<div className={`${styles.counterContainer} ${className}`}>
<div className={styles.label}>YOU ARE VISITOR #</div>
<div className={styles.counter}>
{paddedCount.split('').map((digit, i) => (
<span key={i} className={styles.digit}>{digit}</span>
))}
</div>
<div className={styles.since}>since March 1998</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { VisitorCounter } from './VisitorCounter';

View File

@@ -0,0 +1,48 @@
.webRing {
background: linear-gradient(180deg, #000080 0%, #000040 100%);
border: 3px ridge #c0c0c0;
padding: 15px;
text-align: center;
margin: 20px auto;
max-width: 400px;
}
.title {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
font-weight: bold;
color: #ffff00;
margin-bottom: 10px;
text-shadow: 2px 2px 0 #800000;
}
.navigation {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.link {
font-family: 'VT323', monospace;
font-size: 1rem;
color: #00ffff;
text-decoration: none;
&:hover {
color: #ff00ff;
text-decoration: underline;
}
}
.separator {
color: #808080;
}
.ringLogo {
font-family: 'VT323', monospace;
font-size: 0.8rem;
color: #808080;
font-style: italic;
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import styles from './WebRing.module.scss';
interface WebRingProps {
ringName?: string;
}
export const WebRing: React.FC<WebRingProps> = ({
ringName = 'CoOl SiTeS wEbRiNg'
}) => {
return (
<div className={styles.webRing}>
<div className={styles.title}>
🔗 {ringName} 🔗
</div>
<div className={styles.navigation}>
<a href="#" className={styles.link}> PrEvIoUs</a>
<span className={styles.separator}>|</span>
<a href="#" className={styles.link}>🎲 RaNdOm</a>
<span className={styles.separator}>|</span>
<a href="#" className={styles.link}>NeXt </a>
</div>
<div className={styles.ringLogo}>
[ Part of the {ringName} ]
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { WebRing } from './WebRing';

View File

@@ -0,0 +1,5 @@
export { VisitorCounter } from './VisitorCounter';
export { GuestbookForm } from './GuestbookForm';
export { GuestbookEntry } from './GuestbookEntry';
export { NavLink } from './NavLink';
export { WebRing } from './WebRing';

View File

@@ -0,0 +1,75 @@
.footer {
background: linear-gradient(180deg, #000040 0%, #000080 100%);
border-top: 5px ridge #c0c0c0;
padding: 20px;
}
.divider {
display: flex;
justify-content: center;
margin-bottom: 15px;
}
.content {
text-align: center;
margin-bottom: 15px;
}
.copyright {
font-family: 'VT323', monospace;
font-size: 1rem;
color: #00ff00;
margin-bottom: 5px;
}
.disclaimer {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 0.9rem;
color: #808080;
margin-bottom: 5px;
}
.made {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 0.9rem;
color: #ffff00;
margin-bottom: 5px;
}
.notepad {
font-family: 'VT323', monospace;
background: #c0c0c0;
color: #000;
padding: 2px 5px;
}
.tech {
font-family: 'VT323', monospace;
font-size: 0.8rem;
color: #00ffff;
font-style: italic;
}
.badges {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.customBadge {
font-family: 'Press Start 2P', monospace;
font-size: 6px;
background: #000;
color: #fff;
padding: 5px 8px;
border: 2px outset #808080;
text-align: center;
line-height: 1.4;
&.react {
background: #61dafb;
color: #000;
}
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { RetroGif } from '@/components/atoms/RetroGif';
import styles from './Footer.module.scss';
export const Footer: React.FC = () => {
return (
<footer className={styles.footer}>
<div className={styles.divider}>
<RetroGif type="dividerRainbow" size="medium" />
</div>
<div className={styles.content}>
<p className={styles.copyright}>
© 1998-2026 RiChArDs HoMePaGe
</p>
<p className={styles.disclaimer}>
ThIs SiTe Is NoT aFfIlIaTeD wItH gEoCiTiEs (R.I.P. 😢)
</p>
<p className={styles.made}>
MaDe WiTh 💕 aNd <span className={styles.notepad}>Notepad.exe</span>
</p>
<p className={styles.tech}>
(AcTuAlLy MaDe WiTh Next.js, React, TypeScript, SASS & IndexedDB 🤫)
</p>
</div>
<div className={styles.badges}>
<RetroGif type="netscape" size="small" />
<RetroGif type="ielogo" size="small" />
<div className={styles.customBadge}>
MADE WITH<br/>NEXT.JS
</div>
<div className={styles.customBadge + ' ' + styles.react}>
POWERED BY<br/>REACT
</div>
</div>
</footer>
);
};

View File

@@ -0,0 +1 @@
export { Footer } from './Footer';

View File

@@ -0,0 +1,98 @@
.guestbook {
background: #f0f0f0;
border: 3px ridge #c0c0c0;
padding: 20px;
margin: 20px 0;
}
.header {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.title {
font-family: 'Press Start 2P', cursive;
font-size: 1.2rem;
color: #800080;
text-shadow: 2px 2px 0 #c0c0c0;
}
.intro {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
color: #000080;
text-align: center;
margin-bottom: 15px;
padding: 10px;
background: #ffffcc;
border: 2px dashed #ffcc00;
}
.entries {
margin-top: 20px;
}
.entriesTitle {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
color: #008000;
text-align: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px dashed #008000;
}
.entryList {
max-height: 400px;
overflow-y: auto;
padding-right: 10px;
&::-webkit-scrollbar {
width: 16px;
}
&::-webkit-scrollbar-track {
background: #c0c0c0;
border: 1px inset #808080;
}
&::-webkit-scrollbar-thumb {
background: #808080;
border: 2px outset #c0c0c0;
&:hover {
background: #606060;
}
}
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 20px;
p {
font-family: 'VT323', monospace;
font-size: 1.2rem;
color: #808080;
animation: pulse 1s infinite;
}
}
.noEntries {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
color: #ff0000;
text-align: center;
padding: 20px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

View File

@@ -0,0 +1,65 @@
'use client';
import React from 'react';
import { GuestbookForm } from '@/components/molecules/GuestbookForm';
import { GuestbookEntry } from '@/components/molecules/GuestbookEntry';
import { RetroGif } from '@/components/atoms/RetroGif';
import { BlinkText } from '@/components/atoms/BlinkText';
import type { GuestbookEntry as GuestbookEntryType } from '@/hooks/useGuestbook';
import styles from './Guestbook.module.scss';
interface GuestbookProps {
entries: GuestbookEntryType[];
onSubmit: (name: string, message: string, email?: string) => void;
isLoaded: boolean;
}
export const Guestbook: React.FC<GuestbookProps> = ({
entries,
onSubmit,
isLoaded
}) => {
return (
<section id="guestbook" className={styles.guestbook}>
<div className={styles.header}>
<RetroGif type="guestbook" size="medium" />
<h2 className={styles.title}>
<BlinkText color="#ff0000" speed="slow">📖</BlinkText>
{' '}GuEsTbOoK{' '}
<BlinkText color="#ff0000" speed="slow">📖</BlinkText>
</h2>
<RetroGif type="guestbook" size="medium" />
</div>
<p className={styles.intro}>
PlEaSe SiGn My GuEsTbOoK aNd LeT mE kNoW wHaT yOu ThInK oF mY sItE!!!
I LoVe ReAdInG aLl YoUr MeSsAgEs!!! 💕
</p>
<GuestbookForm onSubmit={onSubmit} />
<div className={styles.entries}>
<h3 className={styles.entriesTitle}>
📜 PrEvIoUs EnTrIeS ({entries.length}) 📜
</h3>
{!isLoaded ? (
<div className={styles.loading}>
<RetroGif type="spinning3d" size="medium" />
<p>LoAdInG...</p>
</div>
) : entries.length === 0 ? (
<p className={styles.noEntries}>
No EnTrIeS yEt! Be ThE fIrSt To SiGn!!!
</p>
) : (
<div className={styles.entryList}>
{entries.map(entry => (
<GuestbookEntry key={entry.id} entry={entry} />
))}
</div>
)}
</div>
</section>
);
};

View File

@@ -0,0 +1 @@
export { Guestbook } from './Guestbook';

View File

@@ -0,0 +1,53 @@
.header {
background: linear-gradient(180deg, #000040 0%, #000080 50%, #000040 100%);
border-bottom: 5px ridge #c0c0c0;
}
.titleSection {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
gap: 20px;
}
.fireLeft,
.fireRight {
display: flex;
gap: 5px;
}
.titleContent {
text-align: center;
}
.title {
font-family: 'Press Start 2P', cursive;
font-size: clamp(1rem, 4vw, 2rem);
margin-bottom: 10px;
text-shadow:
3px 3px 0 #000,
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000;
}
.subtitle {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1.1rem;
color: #00ffff;
text-shadow: 1px 1px 0 #000;
}
.welcomeGif {
display: flex;
justify-content: center;
padding: 10px;
background: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(255,255,255,0.05) 10px,
rgba(255,255,255,0.05) 20px
);
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { MarqueeText } from '@/components/atoms/MarqueeText';
import { BlinkText } from '@/components/atoms/BlinkText';
import { RainbowText } from '@/components/atoms/RainbowText';
import { RetroGif } from '@/components/atoms/RetroGif';
import styles from './Header.module.scss';
export const Header: React.FC = () => {
return (
<header className={styles.header}>
<MarqueeText speed="normal">
WeLcOmE tO mY pErSoNaL hOmEpAgE!!! PlEaSe SiGn My GuEsTbOoK!!!
LaSt UpDaTeD: JaNuArY 21, 2026
YoU aRe ViSiToR nUmBeR [COUNTER]
</MarqueeText>
<div className={styles.titleSection}>
<div className={styles.fireLeft}>
<RetroGif type="fire" size="large" />
<RetroGif type="fire" size="large" />
</div>
<div className={styles.titleContent}>
<h1 className={styles.title}>
<RainbowText>~*~RiChArDs HoMePaGe~*~</RainbowText>
</h1>
<p className={styles.subtitle}>
<BlinkText color="#ff00ff"></BlinkText>
{' '}ThE cOoLeSt SiTe On ThE wOrLd WiDe WeB{' '}
<BlinkText color="#ff00ff"></BlinkText>
</p>
</div>
<div className={styles.fireRight}>
<RetroGif type="fire" size="large" />
<RetroGif type="fire" size="large" />
</div>
</div>
<div className={styles.welcomeGif}>
<RetroGif type="welcome" size="large" />
</div>
</header>
);
};

View File

@@ -0,0 +1 @@
export { Header } from './Header';

View File

@@ -0,0 +1,174 @@
.main {
flex: 1;
padding: 20px;
background:
url("data:image/svg+xml,%3Csvg width='20' height='20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h20v20H0z' fill='%23f0f0f0'/%3E%3Cpath d='M0 0h10v10H0zM10 10h10v10H10z' fill='%23e8e8e8'/%3E%3C/svg%3E");
}
.section {
background: #fff;
border: 3px ridge #c0c0c0;
padding: 20px;
margin-bottom: 20px;
}
.sectionTitle {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1.5rem;
color: #800080;
text-align: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 3px double #800080;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.aboutContent {
display: flex;
gap: 20px;
@media (max-width: 600px) {
flex-direction: column;
align-items: center;
}
}
.aboutPhoto {
flex-shrink: 0;
}
.photoFrame {
border: 5px ridge #c0c0c0;
padding: 5px;
background: #000;
}
.photoPlaceholder {
width: 120px;
height: 150px;
background: linear-gradient(45deg, #333 25%, #444 25%, #444 50%, #333 50%, #333 75%, #444 75%);
background-size: 10px 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #888;
font-family: 'VT323', monospace;
font-size: 0.8rem;
text-align: center;
}
.aboutText {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
line-height: 1.6;
p {
margin-bottom: 10px;
color: #333;
}
}
.favoritesList {
list-style: none;
padding-left: 10px;
li {
margin: 5px 0;
padding: 3px 10px;
background: #ffffcc;
border-left: 3px solid #ffcc00;
}
}
.linksGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.coolLink {
display: block;
padding: 15px;
background: linear-gradient(180deg, #000080 0%, #0000ff 100%);
border: 3px outset #8080ff;
color: #00ffff;
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
text-align: center;
text-decoration: none;
&:hover {
background: linear-gradient(180deg, #800080 0%, #ff00ff 100%);
color: #ffff00;
border-color: #ff80ff;
}
}
.updatesList {
display: flex;
flex-direction: column;
gap: 10px;
}
.updateItem {
display: flex;
gap: 15px;
padding: 10px;
background: #ffffcc;
border: 1px solid #cccc00;
&:nth-child(even) {
background: #ccffcc;
border-color: #00cc00;
}
}
.updateDate {
font-family: 'VT323', monospace;
font-size: 1rem;
color: #808080;
white-space: nowrap;
}
.updateText {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
color: #333;
}
.emailSection {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
padding: 20px;
background: linear-gradient(90deg, #ff00ff, #00ffff, #ffff00, #ff00ff);
background-size: 300% 100%;
animation: gradientShift 5s linear infinite;
border: 5px ridge #c0c0c0;
}
.emailText {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1.2rem;
color: #000;
text-shadow: 1px 1px 0 #fff;
a {
color: #0000ff;
&:hover {
color: #ff0000;
}
}
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}

View File

@@ -0,0 +1,113 @@
import React from 'react';
import { BlinkText } from '@/components/atoms/BlinkText';
import { RetroGif } from '@/components/atoms/RetroGif';
import { RainbowText } from '@/components/atoms/RainbowText';
import { WebRing } from '@/components/molecules/WebRing';
import styles from './MainContent.module.scss';
export const MainContent: React.FC = () => {
return (
<main className={styles.main}>
{/* About Section */}
<section id="about" className={styles.section}>
<h2 className={styles.sectionTitle}>
<RetroGif type="dancing" size="small" />
{' '}AbOuT mE{' '}
<RetroGif type="dancing" size="small" />
</h2>
<div className={styles.aboutContent}>
<div className={styles.aboutPhoto}>
<div className={styles.photoFrame}>
<div className={styles.photoPlaceholder}>
📷
<br />
[photo coming soon]
</div>
</div>
</div>
<div className={styles.aboutText}>
<p>
<BlinkText color="#ff0000">HeLLo!!!</BlinkText> My NaMe Is RiChArD aNd I aM fRoM
tHe UnItEd KiNgDoM!!! 🇬🇧
</p>
<p>
I LoVe CoMpUtErS, pRoGrAmMiNg, aNd MaKiNg CoOl WeBsItEs LiKe ThIs OnE!!!
</p>
<p>
My FaVoRiTe ThInGs:
</p>
<ul className={styles.favoritesList}>
<li>🖥 C++ aNd TyPeScRiPt</li>
<li>🎮 ViDeO gAmEs</li>
<li>🍗 FrIeD cHiCkEn (especially Popeyes!!!)</li>
<li>🔧 BuIlDiNg StUfF</li>
</ul>
</div>
</div>
</section>
{/* Links Section */}
<section id="links" className={styles.section}>
<h2 className={styles.sectionTitle}>
<RetroGif type="hotSite" size="small" />
{' '}CoOl LiNkS{' '}
<RetroGif type="hotSite" size="small" />
</h2>
<div className={styles.linksGrid}>
<a href="https://web.archive.org/web/19990125091252/http://www.geocities.com/" className={styles.coolLink}>
🌐 GeoCities Archive
</a>
<a href="https://www.cameronsworld.net/" className={styles.coolLink}>
Cameron's World
</a>
<a href="https://theoldnet.com/" className={styles.coolLink}>
🕰️ The Old Net
</a>
<a href="https://poolside.fm/" className={styles.coolLink}>
🏊 Poolside FM
</a>
</div>
<WebRing ringName="90s NoStAlGiA rInG" />
</section>
{/* Updates Section */}
<section id="updates" className={styles.section}>
<h2 className={styles.sectionTitle}>
<RetroGif type="new" size="small" />
{' '}LaTeSt UpDaTeS{' '}
<RetroGif type="new" size="small" />
</h2>
<div className={styles.updatesList}>
<div className={styles.updateItem}>
<span className={styles.updateDate}>01/21/2026</span>
<span className={styles.updateText}>
<RainbowText>SiTe LaUnChEd!!!</RainbowText> WeLcOmE tO mY nEw HoMePaGe!!!
</span>
</div>
<div className={styles.updateItem}>
<span className={styles.updateDate}>01/20/2026</span>
<span className={styles.updateText}>
AdDeD gUeStBoOk - PlEaSe SiGn It!!!
</span>
</div>
<div className={styles.updateItem}>
<span className={styles.updateDate}>01/19/2026</span>
<span className={styles.updateText}>
StArTeD bUiLdInG tHiS aWeSoMe SiTe
</span>
</div>
</div>
</section>
{/* Email Section */}
<section className={styles.emailSection}>
<RetroGif type="email" size="medium" />
<p className={styles.emailText}>
EmAiL mE aT: <a href="mailto:webmaster@geocities.example">webmaster@geocities.example</a>
</p>
<RetroGif type="email" size="medium" />
</section>
</main>
);
};

View File

@@ -0,0 +1 @@
export { MainContent } from './MainContent';

View File

@@ -0,0 +1,79 @@
.sidebar {
width: 200px;
background: linear-gradient(180deg, #c0c0c0 0%, #a0a0a0 100%);
border-right: 3px ridge #808080;
padding: 10px;
display: flex;
flex-direction: column;
gap: 15px;
@media (max-width: 768px) {
width: 100%;
border-right: none;
border-bottom: 3px ridge #808080;
}
}
.section {
background: #d4d4d4;
border: 2px inset #808080;
padding: 10px;
}
.sectionTitle {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 0.9rem;
color: #000080;
text-align: center;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px dashed #808080;
}
.nav {
display: flex;
flex-direction: column;
gap: 5px;
}
.divider {
display: flex;
justify-content: center;
padding: 5px 0;
}
.awards {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.browsers {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 5px;
}
.browserNote {
font-family: 'VT323', monospace;
font-size: 0.8rem;
color: #808080;
text-align: center;
}
.construction {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
p {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 0.8rem;
color: #ff0000;
text-align: center;
font-weight: bold;
}
}

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { NavLink } from '@/components/molecules/NavLink';
import { RetroGif } from '@/components/atoms/RetroGif';
import { VisitorCounter } from '@/components/molecules/VisitorCounter';
import styles from './Sidebar.module.scss';
interface SidebarProps {
visitorCount: number;
}
export const Sidebar: React.FC<SidebarProps> = ({ visitorCount }) => {
return (
<aside className={styles.sidebar}>
<div className={styles.section}>
<h3 className={styles.sectionTitle}>📍 NaViGaTiOn 📍</h3>
<nav className={styles.nav}>
<NavLink href="#home" icon="🏠">HoMe</NavLink>
<NavLink href="#about" icon="👤">AbOuT mE</NavLink>
<NavLink href="#links" icon="🔗" isHot>CoOl LiNkS</NavLink>
<NavLink href="#guestbook" icon="📖">GuEsTbOoK</NavLink>
<NavLink href="#updates" icon="📰" isNew>UpDaTeS</NavLink>
</nav>
</div>
<div className={styles.divider}>
<RetroGif type="dividerRainbow" size="medium" />
</div>
<div className={styles.section}>
<VisitorCounter count={visitorCount} />
</div>
<div className={styles.section}>
<h3 className={styles.sectionTitle}>🏆 AwArDs 🏆</h3>
<div className={styles.awards}>
<RetroGif type="coolSite" size="medium" />
<RetroGif type="hotSite" size="medium" />
</div>
</div>
<div className={styles.divider}>
<RetroGif type="dividerRainbow" size="medium" />
</div>
<div className={styles.section}>
<h3 className={styles.sectionTitle}>🌐 BeSt ViEwEd WiTh 🌐</h3>
<div className={styles.browsers}>
<RetroGif type="netscape" size="small" />
<RetroGif type="ielogo" size="small" />
</div>
<p className={styles.browserNote}>800x600 resolution</p>
</div>
<div className={styles.section}>
<div className={styles.construction}>
<RetroGif type="underConstruction" size="medium" />
<p>ThIs SiTe Is AlWaYs<br/>UnDeR cOnStRuCtIoN!</p>
<RetroGif type="underConstruction2" size="medium" />
</div>
</div>
</aside>
);
};

View File

@@ -0,0 +1 @@
export { Sidebar } from './Sidebar';

View File

@@ -0,0 +1,5 @@
export { Sidebar } from './Sidebar';
export { Header } from './Header';
export { Guestbook } from './Guestbook';
export { MainContent } from './MainContent';
export { Footer } from './Footer';

View File

@@ -0,0 +1,21 @@
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.body {
flex: 1;
display: flex;
@media (max-width: 768px) {
flex-direction: column;
}
}
.mainArea {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}

View File

@@ -0,0 +1,46 @@
'use client';
import React from 'react';
import { Header } from '@/components/organisms/Header';
import { Sidebar } from '@/components/organisms/Sidebar';
import { MainContent } from '@/components/organisms/MainContent';
import { Guestbook } from '@/components/organisms/Guestbook';
import { Footer } from '@/components/organisms/Footer';
import type { GuestbookEntry } from '@/hooks/useGuestbook';
import styles from './MainLayout.module.scss';
interface MainLayoutProps {
visitorCount: number;
guestbookEntries: GuestbookEntry[];
onGuestbookSubmit: (name: string, message: string, email?: string) => void;
isGuestbookLoaded: boolean;
}
export const MainLayout: React.FC<MainLayoutProps> = ({
visitorCount,
guestbookEntries,
onGuestbookSubmit,
isGuestbookLoaded
}) => {
return (
<div className={styles.layout}>
<Header />
<div className={styles.body}>
<Sidebar visitorCount={visitorCount} />
<div className={styles.mainArea}>
<MainContent />
<Guestbook
entries={guestbookEntries}
onSubmit={onGuestbookSubmit}
isLoaded={isGuestbookLoaded}
/>
</div>
</div>
<Footer />
</div>
);
};

View File

@@ -0,0 +1 @@
export { MainLayout } from './MainLayout';

View File

@@ -0,0 +1,242 @@
.splash {
min-height: 100vh;
background:
radial-gradient(ellipse at center, #1a0033 0%, #000011 70%),
linear-gradient(180deg, #000022 0%, #110022 100%);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
// Starfield
.stars, .stars2 {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.stars {
background-image:
radial-gradient(2px 2px at 20px 30px, white, transparent),
radial-gradient(2px 2px at 40px 70px, white, transparent),
radial-gradient(1px 1px at 90px 40px, white, transparent),
radial-gradient(2px 2px at 160px 120px, white, transparent),
radial-gradient(1px 1px at 230px 80px, white, transparent),
radial-gradient(2px 2px at 300px 150px, white, transparent),
radial-gradient(1px 1px at 400px 60px, white, transparent),
radial-gradient(2px 2px at 500px 180px, white, transparent);
background-size: 550px 200px;
animation: twinkle 5s infinite;
}
.stars2 {
background-image:
radial-gradient(1px 1px at 50px 100px, #ff00ff, transparent),
radial-gradient(1px 1px at 150px 50px, #00ffff, transparent),
radial-gradient(1px 1px at 250px 150px, #ffff00, transparent),
radial-gradient(1px 1px at 350px 80px, #ff00ff, transparent),
radial-gradient(1px 1px at 450px 120px, #00ffff, transparent);
background-size: 500px 200px;
animation: twinkle 3s infinite reverse;
}
@keyframes twinkle {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
// Cursor trail
.cursorStar {
position: fixed;
pointer-events: none;
font-size: 12px;
z-index: 1000;
transform: translate(-50%, -50%);
}
// Content
.content {
position: relative;
z-index: 1;
text-align: center;
padding: 20px;
max-width: 600px;
}
.topGifs {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
}
.title {
font-family: 'Press Start 2P', cursive;
font-size: clamp(1.5rem, 5vw, 2.5rem);
margin-bottom: 10px;
text-shadow:
0 0 10px #ff00ff,
0 0 20px #ff00ff,
0 0 30px #ff00ff;
}
.subtitle {
font-family: 'Press Start 2P', cursive;
font-size: 1.5rem;
margin-bottom: 10px;
}
.siteTitle {
font-family: 'Press Start 2P', cursive;
font-size: clamp(1rem, 4vw, 2rem);
color: #ffff00;
margin-bottom: 20px;
text-shadow:
3px 3px 0 #ff0000,
-1px -1px 0 #ff0000;
}
// Glitch effect
.glitch {
position: relative;
animation: glitch 2s infinite;
&::before,
&::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&::before {
color: #ff0000;
animation: glitchTop 2s infinite;
clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%);
}
&::after {
color: #00ffff;
animation: glitchBottom 2s infinite;
clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%);
}
}
@keyframes glitch {
0%, 100% { transform: translate(0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(-2px, -2px); }
60% { transform: translate(2px, 2px); }
80% { transform: translate(2px, -2px); }
}
@keyframes glitchTop {
0%, 100% { transform: translate(0); }
20% { transform: translate(2px, -2px); }
40% { transform: translate(-2px, 2px); }
}
@keyframes glitchBottom {
0%, 100% { transform: translate(0); }
60% { transform: translate(-2px, 2px); }
80% { transform: translate(2px, -2px); }
}
.dancing {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
}
.enterSection {
background: rgba(0, 0, 128, 0.5);
border: 3px ridge #c0c0c0;
padding: 20px;
margin-bottom: 20px;
}
.enterText {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1.2rem;
color: #00ff00;
margin-bottom: 15px;
}
.visitorText {
font-family: 'VT323', monospace;
font-size: 1rem;
color: #808080;
margin-top: 10px;
}
.warning {
background: #ffff00;
border: 4px dashed #ff0000;
padding: 15px;
margin-bottom: 20px;
p {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 1rem;
color: #000;
margin: 10px 0;
}
ul {
list-style: none;
padding: 0;
li {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 0.9rem;
color: #333;
margin: 5px 0;
}
}
}
.bottomSection {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.construction {
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
font-size: 0.9rem;
color: #ffff00;
}
.browserBadges {
margin-top: 20px;
p {
font-family: 'VT323', monospace;
font-size: 0.9rem;
color: #808080;
margin-bottom: 10px;
}
}
.badges {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 10px;
}
.resolution {
font-family: 'VT323', monospace;
font-size: 0.8rem !important;
color: #606060 !important;
}

View File

@@ -0,0 +1,141 @@
'use client';
import React, { useState, useEffect } from 'react';
import { RetroButton } from '@/components/atoms/RetroButton';
import { BlinkText } from '@/components/atoms/BlinkText';
import { RainbowText } from '@/components/atoms/RainbowText';
import { RetroGif } from '@/components/atoms/RetroGif';
import styles from './SplashScreen.module.scss';
interface SplashScreenProps {
onEnter: () => void;
visitorCount: number;
}
export const SplashScreen: React.FC<SplashScreenProps> = ({ onEnter, visitorCount }) => {
const [showWarning, setShowWarning] = useState(false);
const [cursorTrail, setCursorTrail] = useState<Array<{x: number, y: number, id: number}>>([]);
useEffect(() => {
const timer = setTimeout(() => setShowWarning(true), 2000);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setCursorTrail(prev => {
const newTrail = [...prev, { x: e.clientX, y: e.clientY, id: Date.now() }];
return newTrail.slice(-20);
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
useEffect(() => {
const interval = setInterval(() => {
setCursorTrail(prev => prev.slice(1));
}, 50);
return () => clearInterval(interval);
}, []);
return (
<div className={styles.splash}>
{/* Cursor trail */}
{cursorTrail.map((point, i) => (
<div
key={point.id}
className={styles.cursorStar}
style={{
left: point.x,
top: point.y,
opacity: i / cursorTrail.length,
transform: `scale(${0.5 + (i / cursorTrail.length) * 0.5})`
}}
>
</div>
))}
{/* Starfield background effect */}
<div className={styles.stars}></div>
<div className={styles.stars2}></div>
{/* Main content */}
<div className={styles.content}>
<div className={styles.topGifs}>
<RetroGif type="skull" size="large" />
<RetroGif type="fire" size="large" />
<RetroGif type="skull" size="large" />
</div>
<h1 className={styles.title}>
<RainbowText>~*~WELCOME~*~</RainbowText>
</h1>
<h2 className={styles.subtitle}>
<BlinkText color="#00ffff" speed="slow">TO</BlinkText>
</h2>
<h1 className={styles.siteTitle}>
<span className={styles.glitch} data-text="RiChArDs HoMePaGe">
RiChArDs HoMePaGe
</span>
</h1>
<div className={styles.dancing}>
<RetroGif type="dancing" size="large" />
<RetroGif type="dancing" size="large" />
<RetroGif type="dancing" size="large" />
</div>
<div className={styles.enterSection}>
<p className={styles.enterText}>
<BlinkText color="#ff00ff"></BlinkText>
{' '}ClIcK tO eNtEr ThE sItE{' '}
<BlinkText color="#ff00ff"></BlinkText>
</p>
<RetroButton onClick={onEnter} variant="primary" size="large">
🚀 ENTER 🚀
</RetroButton>
<p className={styles.visitorText}>
YoU wIlL bE vIsItOr #{visitorCount + 1}!
</p>
</div>
{showWarning && (
<div className={styles.warning}>
<BlinkText color="#ff0000" speed="fast"> WARNING </BlinkText>
<p>ThIs SiTe CoNtAiNs:</p>
<ul>
<li>🔥 AnImAtEd GiFs</li>
<li>🌈 RaInBoW tExT</li>
<li> BlInKiNg ThInGs</li>
<li>🎵 MaYbE sOuNd (JK nO aUtOpLaY)</li>
</ul>
</div>
)}
<div className={styles.bottomSection}>
<RetroGif type="underConstruction" size="medium" />
<p className={styles.construction}>
SiTe AlWaYs UnDeR cOnStRuCtIoN!!!
</p>
<RetroGif type="underConstruction2" size="medium" />
</div>
<div className={styles.browserBadges}>
<p>BeSt ViEwEd WiTh:</p>
<div className={styles.badges}>
<RetroGif type="netscape" size="small" />
<RetroGif type="ielogo" size="small" />
</div>
<p className={styles.resolution}>800 x 600 256 CoLoRs</p>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { SplashScreen } from './SplashScreen';

View File

@@ -0,0 +1,2 @@
export { SplashScreen } from './SplashScreen';
export { MainLayout } from './MainLayout';

View File

@@ -0,0 +1,4 @@
export { useIndexedDB } from './useIndexedDB';
export { useVisitorCounter } from './useVisitorCounter';
export { useGuestbook } from './useGuestbook';
export type { GuestbookEntry } from './useGuestbook';

View File

@@ -0,0 +1,96 @@
import { useState, useEffect, useCallback } from 'react';
import { useIndexedDB } from './useIndexedDB';
export interface GuestbookEntry {
id: string;
name: string;
message: string;
date: string;
email?: string;
}
const DEFAULT_ENTRIES: GuestbookEntry[] = [
{
id: 'seed-1',
name: 'SuRfErDuDe1999',
message: 'CoOl SiTe DuDe!!! ChEcK oUt My PaGe At GeOcItIeS!!!',
date: '1998-03-15T10:30:00Z',
email: 'surferdude@aol.com'
},
{
id: 'seed-2',
name: 'xX_DaRkAnGeL_Xx',
message: 'LoVe ThE gRaPhIcS!!! <3 <3 <3',
date: '1998-04-22T14:15:00Z',
},
{
id: 'seed-3',
name: 'WebMaster2000',
message: 'GrEaT uSe Of FrAmEs! HoW dId YoU mAkE tHe AnImAtEd GiFs??',
date: '1998-06-10T09:45:00Z',
email: 'webmaster@geocities.com'
},
{
id: 'seed-4',
name: 'CyBeRpUnK_HaCkEr',
message: 'ThIs SiTe Is ToTaLlY rAdIcAl!!! KeEp Up ThE gOoD wOrK!!!',
date: '1999-01-01T00:00:00Z',
}
];
export function useGuestbook() {
const [entries, setEntries] = useState<GuestbookEntry[]>([]);
const [isLoaded, setIsLoaded] = useState(false);
const { getAllItems, setItem, isReady } = useIndexedDB('guestbook');
useEffect(() => {
if (!isReady) return;
const loadEntries = async () => {
try {
const stored = await getAllItems();
if (stored.length === 0) {
// Seed with default entries
for (const entry of DEFAULT_ENTRIES) {
await setItem(entry as any);
}
setEntries(DEFAULT_ENTRIES);
} else {
setEntries(stored as GuestbookEntry[]);
}
} catch (error) {
console.error('Failed to load guestbook:', error);
setEntries(DEFAULT_ENTRIES);
} finally {
setIsLoaded(true);
}
};
loadEntries();
}, [isReady, getAllItems, setItem]);
const addEntry = useCallback(async (name: string, message: string, email?: string) => {
const newEntry: GuestbookEntry = {
id: `entry-${Date.now()}`,
name,
message,
date: new Date().toISOString(),
email,
};
try {
await setItem(newEntry as any);
setEntries(prev => [newEntry, ...prev]);
} catch (error) {
console.error('Failed to add guestbook entry:', error);
}
return newEntry;
}, [setItem]);
const sortedEntries = [...entries].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
return { entries: sortedEntries, addEntry, isLoaded };
}

View File

@@ -0,0 +1,108 @@
import { useState, useEffect, useCallback } from 'react';
const DB_NAME = 'GeoCitiesDB';
const DB_VERSION = 1;
interface DBSchema {
visitors: { id: string; count: number };
guestbook: { id: string; name: string; message: string; date: string; email?: string };
settings: { id: string; value: unknown };
}
type StoreName = keyof DBSchema;
let dbInstance: IDBDatabase | null = null;
let dbPromise: Promise<IDBDatabase> | null = null;
const openDB = (): Promise<IDBDatabase> => {
if (dbInstance) return Promise.resolve(dbInstance);
if (dbPromise) return dbPromise;
dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
dbInstance = request.result;
resolve(dbInstance);
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('visitors')) {
db.createObjectStore('visitors', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('guestbook')) {
const guestbookStore = db.createObjectStore('guestbook', { keyPath: 'id' });
guestbookStore.createIndex('date', 'date', { unique: false });
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'id' });
}
};
});
return dbPromise;
};
export function useIndexedDB<T extends StoreName>(storeName: T) {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
openDB().then(() => setIsReady(true)).catch(console.error);
}, []);
const getItem = useCallback(async <K extends string>(key: K): Promise<DBSchema[T] | undefined> => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}, [storeName]);
const setItem = useCallback(async (value: DBSchema[T]): Promise<void> => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(value);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}, [storeName]);
const getAllItems = useCallback(async (): Promise<DBSchema[T][]> => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}, [storeName]);
const deleteItem = useCallback(async (key: string): Promise<void> => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}, [storeName]);
return { getItem, setItem, getAllItems, deleteItem, isReady };
}

View File

@@ -0,0 +1,47 @@
import { useState, useEffect, useCallback } from 'react';
import { useIndexedDB } from './useIndexedDB';
const VISITOR_KEY = 'total_visitors';
const INITIAL_COUNT = 1337; // Start with a believable 90s number
export function useVisitorCounter() {
const [visitorCount, setVisitorCount] = useState<number>(INITIAL_COUNT);
const [isLoaded, setIsLoaded] = useState(false);
const { getItem, setItem, isReady } = useIndexedDB('visitors');
useEffect(() => {
if (!isReady) return;
const loadCount = async () => {
try {
const record = await getItem(VISITOR_KEY);
if (record) {
setVisitorCount(record.count);
} else {
await setItem({ id: VISITOR_KEY, count: INITIAL_COUNT });
}
} catch (error) {
console.error('Failed to load visitor count:', error);
} finally {
setIsLoaded(true);
}
};
loadCount();
}, [isReady, getItem, setItem]);
const incrementVisitor = useCallback(async () => {
const newCount = visitorCount + 1;
setVisitorCount(newCount);
try {
await setItem({ id: VISITOR_KEY, count: newCount });
} catch (error) {
console.error('Failed to save visitor count:', error);
}
return newCount;
}, [visitorCount, setItem]);
return { visitorCount, incrementVisitor, isLoaded };
}

View File

@@ -0,0 +1,14 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Allow loading images from archive.org for classic gifs
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'web.archive.org',
},
],
},
};
module.exports = nextConfig;

906
packages/geocities-app/package-lock.json generated Normal file
View File

@@ -0,0 +1,906 @@
{
"name": "geocities-app",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "geocities-app",
"version": "0.1.0",
"dependencies": {
"next": "^14.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"sass": "^1.77.0",
"typescript": "^5"
}
},
"node_modules/@next/env": {
"version": "14.2.35",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
"integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
"integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
"integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
"integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
"integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
"integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
"integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
"integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
"integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.4.tgz",
"integrity": "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.4",
"@parcel/watcher-darwin-arm64": "2.5.4",
"@parcel/watcher-darwin-x64": "2.5.4",
"@parcel/watcher-freebsd-x64": "2.5.4",
"@parcel/watcher-linux-arm-glibc": "2.5.4",
"@parcel/watcher-linux-arm-musl": "2.5.4",
"@parcel/watcher-linux-arm64-glibc": "2.5.4",
"@parcel/watcher-linux-arm64-musl": "2.5.4",
"@parcel/watcher-linux-x64-glibc": "2.5.4",
"@parcel/watcher-linux-x64-musl": "2.5.4",
"@parcel/watcher-win32-arm64": "2.5.4",
"@parcel/watcher-win32-ia32": "2.5.4",
"@parcel/watcher-win32-x64": "2.5.4"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.4.tgz",
"integrity": "sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.4.tgz",
"integrity": "sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.4.tgz",
"integrity": "sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.4.tgz",
"integrity": "sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.4.tgz",
"integrity": "sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.4.tgz",
"integrity": "sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.4.tgz",
"integrity": "sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.4.tgz",
"integrity": "sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.4.tgz",
"integrity": "sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.4.tgz",
"integrity": "sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.4.tgz",
"integrity": "sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.4.tgz",
"integrity": "sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.4.tgz",
"integrity": "sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "20.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001765",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
"integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"license": "MIT",
"optional": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next": {
"version": "14.2.35",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
"integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.35",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.33",
"@next/swc-darwin-x64": "14.2.33",
"@next/swc-linux-arm64-gnu": "14.2.33",
"@next/swc-linux-arm64-musl": "14.2.33",
"@next/swc-linux-x64-gnu": "14.2.33",
"@next/swc-linux-x64-musl": "14.2.33",
"@next/swc-win32-arm64-msvc": "14.2.33",
"@next/swc-win32-ia32-msvc": "14.2.33",
"@next/swc-win32-x64-msvc": "14.2.33"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass": {
"version": "1.97.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz",
"integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "geocities-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^14.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"sass": "^1.77.0",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,169 @@
// Global Reset & Base Styles for our 90s Masterpiece
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
// 90s Color Palette
--color-navy: #000080;
--color-purple: #800080;
--color-teal: #008080;
--color-lime: #00ff00;
--color-yellow: #ffff00;
--color-cyan: #00ffff;
--color-magenta: #ff00ff;
--color-red: #ff0000;
--color-silver: #c0c0c0;
--color-gray: #808080;
// Windows 95/98 Colors
--win95-bg: #c0c0c0;
--win95-highlight: #ffffff;
--win95-shadow: #808080;
--win95-dark-shadow: #0a0a0a;
--win95-light: #dfdfdf;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Comic Neue', 'Comic Sans MS', 'MS Sans Serif', cursive;
font-size: 16px;
line-height: 1.4;
color: #000;
background: var(--win95-bg);
// Custom 90s cursor
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='white' stroke='black' d='M4 4l20 8-8 4 4 12-4-4-4 4-4-12-4 4z'/%3E%3C/svg%3E"), auto;
}
// Link styling
a {
color: #0000ff;
text-decoration: underline;
&:visited {
color: #800080;
}
&:hover {
color: #ff00ff;
cursor: pointer;
}
&:active {
color: #ff0000;
}
}
// Classic scrollbar styling for webkit browsers
::-webkit-scrollbar {
width: 16px;
height: 16px;
}
::-webkit-scrollbar-track {
background: #c0c0c0;
border: 1px solid #808080;
}
::-webkit-scrollbar-thumb {
background: #c0c0c0;
border: 2px outset #dfdfdf;
&:hover {
background: #a0a0a0;
}
}
::-webkit-scrollbar-button {
background: #c0c0c0;
border: 2px outset #dfdfdf;
width: 16px;
height: 16px;
}
// Firefox scrollbar
* {
scrollbar-width: auto;
scrollbar-color: #c0c0c0 #808080;
}
// Selection styling
::selection {
background: #000080;
color: #ffffff;
}
// Image styling
img {
max-width: 100%;
height: auto;
}
// Utility classes
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
// Classic Windows border utilities
.border-outset {
border: 2px outset var(--win95-light);
}
.border-inset {
border: 2px inset var(--win95-shadow);
}
.border-ridge {
border: 3px ridge var(--win95-bg);
}
.border-groove {
border: 3px groove var(--win95-bg);
}
// Animations
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
@keyframes marquee {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
@keyframes rainbow {
0% { color: #ff0000; }
14% { color: #ff7f00; }
28% { color: #ffff00; }
42% { color: #00ff00; }
57% { color: #0000ff; }
71% { color: #4b0082; }
85% { color: #9400d3; }
100% { color: #ff0000; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}