mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
geocities
This commit is contained in:
101
packages/geocities-app/README.md
Normal file
101
packages/geocities-app/README.md
Normal 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)*
|
||||
29
packages/geocities-app/app/layout.tsx
Normal file
29
packages/geocities-app/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
43
packages/geocities-app/app/page.tsx
Normal file
43
packages/geocities-app/app/page.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { BlinkText } from './BlinkText';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { MarqueeText } from './MarqueeText';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { RainbowText } from './RainbowText';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { RetroButton } from './RetroButton';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,2 @@
|
||||
export { RetroGif, GIF_SOURCES } from './RetroGif';
|
||||
export type { GifType } from './RetroGif';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { RetroInput } from './RetroInput';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { RetroTextarea } from './RetroTextarea';
|
||||
8
packages/geocities-app/components/atoms/index.ts
Normal file
8
packages/geocities-app/components/atoms/index.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { GuestbookEntry } from './GuestbookEntry';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { GuestbookForm } from './GuestbookForm';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { NavLink } from './NavLink';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { VisitorCounter } from './VisitorCounter';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { WebRing } from './WebRing';
|
||||
5
packages/geocities-app/components/molecules/index.ts
Normal file
5
packages/geocities-app/components/molecules/index.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Footer } from './Footer';
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Guestbook } from './Guestbook';
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Header } from './Header';
|
||||
@@ -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%; }
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { MainContent } from './MainContent';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Sidebar } from './Sidebar';
|
||||
5
packages/geocities-app/components/organisms/index.ts
Normal file
5
packages/geocities-app/components/organisms/index.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { MainLayout } from './MainLayout';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SplashScreen } from './SplashScreen';
|
||||
2
packages/geocities-app/components/templates/index.ts
Normal file
2
packages/geocities-app/components/templates/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { SplashScreen } from './SplashScreen';
|
||||
export { MainLayout } from './MainLayout';
|
||||
4
packages/geocities-app/hooks/index.ts
Normal file
4
packages/geocities-app/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { useIndexedDB } from './useIndexedDB';
|
||||
export { useVisitorCounter } from './useVisitorCounter';
|
||||
export { useGuestbook } from './useGuestbook';
|
||||
export type { GuestbookEntry } from './useGuestbook';
|
||||
96
packages/geocities-app/hooks/useGuestbook.ts
Normal file
96
packages/geocities-app/hooks/useGuestbook.ts
Normal 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 };
|
||||
}
|
||||
108
packages/geocities-app/hooks/useIndexedDB.ts
Normal file
108
packages/geocities-app/hooks/useIndexedDB.ts
Normal 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 };
|
||||
}
|
||||
47
packages/geocities-app/hooks/useVisitorCounter.ts
Normal file
47
packages/geocities-app/hooks/useVisitorCounter.ts
Normal 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 };
|
||||
}
|
||||
14
packages/geocities-app/next.config.js
Normal file
14
packages/geocities-app/next.config.js
Normal 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
906
packages/geocities-app/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
packages/geocities-app/package.json
Normal file
23
packages/geocities-app/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
169
packages/geocities-app/styles/globals.scss
Normal file
169
packages/geocities-app/styles/globals.scss
Normal 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); }
|
||||
}
|
||||
26
packages/geocities-app/tsconfig.json
Normal file
26
packages/geocities-app/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user