rmaps-online/src/app/page.tsx

468 lines
21 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { nanoid } from 'nanoid';
import { AuthButton } from '@/components/AuthButton';
import { useAuthStore } from '@/stores/auth';
// Emoji options for avatars
const EMOJI_OPTIONS = ['🐙', '🦊', '🐻', '🐱', '🦝', '🐸', '🦉', '🐧', '🦋', '🐝'];
// Generate a URL-safe room slug
function generateSlug(): string {
return nanoid(8).toLowerCase();
}
export default function HomePage() {
const router = useRouter();
const { isAuthenticated, username: authUsername } = useAuthStore();
const [isCreating, setIsCreating] = useState(false);
const [joinSlug, setJoinSlug] = useState('');
const [name, setName] = useState('');
const [emoji, setEmoji] = useState('');
const [roomName, setRoomName] = useState('');
const [isLoaded, setIsLoaded] = useState(false);
const [lastRoom, setLastRoom] = useState<string | null>(null);
// Load saved user info from localStorage on mount
// If opened as installed PWA (standalone mode), auto-redirect to last room
useEffect(() => {
let loadedEmoji = '';
try {
const stored = localStorage.getItem('rmaps_user');
if (stored) {
const user = JSON.parse(stored);
if (user.name) setName(user.name);
if (user.emoji) {
setEmoji(user.emoji);
loadedEmoji = user.emoji;
}
}
// Load last visited room
const lastVisited = localStorage.getItem('rmaps_last_room');
if (lastVisited) {
setLastRoom(lastVisited);
// Auto-redirect if running as installed PWA and user has saved info
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|| (navigator as unknown as { standalone?: boolean }).standalone === true;
if (isStandalone && stored) {
router.push(`/${lastVisited}`);
return;
}
}
} catch {
// Ignore parse errors
}
// Set random emoji if none loaded
if (!loadedEmoji) {
setEmoji(EMOJI_OPTIONS[Math.floor(Math.random() * EMOJI_OPTIONS.length)]);
}
setIsLoaded(true);
}, [router]);
// Auto-fill name from EncryptID when authenticated
useEffect(() => {
if (isAuthenticated && authUsername && !name) {
setName(authUsername);
}
}, [isAuthenticated, authUsername, name]);
const handleCreateRoom = async () => {
if (!name.trim()) return;
// Require EncryptID auth to create rooms
if (!isAuthenticated) {
alert('Please sign in with EncryptID to create a room.');
return;
}
const slug = roomName.trim()
? roomName.toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 20)
: generateSlug();
// Store user info in localStorage for the session
localStorage.setItem('rmaps_user', JSON.stringify({ name, emoji }));
localStorage.setItem('rmaps_last_room', slug);
// Navigate to the room (will create it if it doesn't exist)
router.push(`/${slug}`);
};
const handleJoinRoom = () => {
if (!name.trim() || !joinSlug.trim()) return;
localStorage.setItem('rmaps_user', JSON.stringify({ name, emoji }));
// Clean the slug
const cleanSlug = joinSlug.toLowerCase().replace(/[^a-z0-9-]/g, '');
localStorage.setItem('rmaps_last_room', cleanSlug);
router.push(`/${cleanSlug}`);
};
const handleRejoinLastRoom = () => {
if (!name.trim() || !lastRoom) return;
localStorage.setItem('rmaps_user', JSON.stringify({ name, emoji }));
router.push(`/${lastRoom}`);
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
{/* ── Header Nav ─────────────────────────────────────────── */}
<nav className="border-b border-slate-700/50 backdrop-blur-sm sticky top-0 z-50 bg-slate-900/90">
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-lg flex items-center justify-center font-bold text-slate-900 text-sm">
rM
</div>
<span className="font-semibold text-lg">
<span className="text-rmaps-primary">r</span>Maps
</span>
</Link>
<div className="flex items-center gap-4">
<Link
href="/demo"
className="text-sm text-slate-300 hover:text-white transition-colors"
>
Demo
</Link>
<a
href="#get-started"
className="text-sm px-4 py-2 bg-emerald-600 hover:bg-emerald-500 rounded-lg transition-colors font-medium"
>
Create Map
</a>
<AuthButton />
</div>
</div>
</nav>
{/* ── Hero Section ─────────────────────────────────────────── */}
<section className="relative overflow-hidden">
{/* Subtle background grid */}
<div className="absolute inset-0 opacity-[0.03]" style={{
backgroundImage: 'radial-gradient(circle, #10b981 1px, transparent 1px)',
backgroundSize: '40px 40px',
}} />
<div className="relative max-w-5xl mx-auto px-6 pt-16 pb-12 text-center">
{/* Logo */}
<div className="flex items-center justify-center gap-3 mb-8">
<div className="w-12 h-12 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-xl flex items-center justify-center font-bold text-slate-900 text-lg">
rM
</div>
<h1 className="text-4xl sm:text-5xl font-bold">
<span className="text-rmaps-primary">r</span>Maps
</h1>
</div>
{/* Headline */}
<h2 className="text-3xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-emerald-300 via-teal-200 to-cyan-300 bg-clip-text text-transparent leading-tight">
Find Your Friends, Anywhere
</h2>
<p className="text-lg sm:text-xl text-white/60 max-w-2xl mx-auto mb-8">
Privacy-first real-time location sharing for events, festivals, and camps.
See where your crew is without trusting a central server.
</p>
{/* CTA buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-6">
<Link
href="/demo"
className="px-8 py-3.5 bg-emerald-600 hover:bg-emerald-500 text-white font-medium rounded-xl transition-all shadow-lg shadow-emerald-900/30 text-lg"
>
Try the Demo
</Link>
<a
href="#get-started"
className="px-8 py-3.5 bg-white/5 hover:bg-white/10 text-white font-medium rounded-xl transition-colors border border-white/10 text-lg"
>
Get Started
</a>
</div>
<p className="text-sm text-white/40">
No app install. No sign-up required to join a room.
</p>
</div>
</section>
{/* ── Feature Cards ────────────────────────────────────────── */}
<section className="max-w-5xl mx-auto px-6 pb-16">
<div className="grid sm:grid-cols-3 gap-6">
{/* Real-time GPS */}
<div className="bg-slate-800/40 rounded-2xl border border-slate-700/40 p-6 text-center hover:border-emerald-500/30 transition-colors">
<div className="w-14 h-14 mx-auto mb-4 bg-emerald-500/10 rounded-xl flex items-center justify-center">
<svg className="w-7 h-7 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold mb-2 text-white">Real-time GPS</h3>
<p className="text-sm text-white/50">
Share your live location with friends. See everyone on the map updating in real time as you move.
</p>
</div>
{/* Event Maps */}
<div className="bg-slate-800/40 rounded-2xl border border-slate-700/40 p-6 text-center hover:border-indigo-500/30 transition-colors">
<div className="w-14 h-14 mx-auto mb-4 bg-indigo-500/10 rounded-xl flex items-center justify-center">
<svg className="w-7 h-7 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold mb-2 text-white">Event Maps</h3>
<p className="text-sm text-white/50">
Navigate festivals, camps, and conferences. Custom maps with labeled stages, food courts, and meeting points.
</p>
</div>
{/* Privacy First */}
<div className="bg-slate-800/40 rounded-2xl border border-slate-700/40 p-6 text-center hover:border-rose-500/30 transition-colors">
<div className="w-14 h-14 mx-auto mb-4 bg-rose-500/10 rounded-xl flex items-center justify-center">
<svg className="w-7 h-7 text-rose-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
</div>
<h3 className="text-lg font-semibold mb-2 text-white">Privacy First</h3>
<p className="text-sm text-white/50">
Zero-knowledge architecture. You control who sees you. Go invisible anytime. No tracking, no data collection.
</p>
</div>
</div>
</section>
{/* ── How It Works ─────────────────────────────────────────── */}
<section className="max-w-5xl mx-auto px-6 pb-20">
<h2 className="text-2xl sm:text-3xl font-bold text-center mb-10 text-white">How It Works</h2>
<div className="grid sm:grid-cols-3 gap-8">
{/* Step 1 */}
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 rounded-full bg-emerald-500/15 border border-emerald-500/30 flex items-center justify-center text-emerald-400 font-bold text-lg mb-4">
1
</div>
<h3 className="font-semibold text-white mb-2">Create a Map Room</h3>
<p className="text-sm text-white/50">
Sign in and create a room for your event. Get a shareable link like <span className="text-white/70 font-mono text-xs">rmaps.online/ccc-camp</span>
</p>
</div>
{/* Step 2 */}
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 rounded-full bg-indigo-500/15 border border-indigo-500/30 flex items-center justify-center text-indigo-400 font-bold text-lg mb-4">
2
</div>
<h3 className="font-semibold text-white mb-2">Share with Friends</h3>
<p className="text-sm text-white/50">
Send the link to your crew. They join instantly from any device -- no app download, no account needed.
</p>
</div>
{/* Step 3 */}
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 rounded-full bg-cyan-500/15 border border-cyan-500/30 flex items-center justify-center text-cyan-400 font-bold text-lg mb-4">
3
</div>
<h3 className="font-semibold text-white mb-2">Find Each Other</h3>
<p className="text-sm text-white/50">
See everyone on the map in real time. Set your status, share meeting points, and never lose your friends again.
</p>
</div>
</div>
{/* Connector lines (decorative) */}
<div className="hidden sm:flex items-center justify-center mt-8">
<div className="flex items-center gap-2 text-white/20 text-sm">
<span>Built for</span>
<a
href="https://events.ccc.de/"
target="_blank"
rel="noopener noreferrer"
className="text-rmaps-primary/60 hover:text-rmaps-primary transition-colors"
>
CCC events
</a>
<span>and beyond</span>
</div>
</div>
</section>
{/* ── Get Started (existing login/room interface) ──────────── */}
<section id="get-started" className="scroll-mt-8 pb-16">
<main className="flex flex-col items-center px-4">
<div className="max-w-md w-full space-y-8">
{/* Section heading */}
<div className="text-center">
<h2 className="text-2xl sm:text-3xl font-bold mb-2 text-white">Get Started</h2>
<p className="text-white/50 text-sm mb-4">Create or join a map room</p>
<div className="mt-3">
<AuthButton />
</div>
</div>
{/* Quick Rejoin Card - show when user has saved info and last room */}
{isLoaded && name && lastRoom && (
<div className="room-panel rounded-2xl p-6">
<p className="text-white/60 text-sm text-center mb-4">Welcome back, {name}!</p>
<button
onClick={handleRejoinLastRoom}
className="btn-primary w-full text-lg py-3 flex items-center justify-center gap-2"
>
<span className="text-xl">{emoji}</span>
<span>Rejoin /{lastRoom}</span>
</button>
<p className="text-white/40 text-xs text-center mt-3">
Or create/join a different room below
</p>
</div>
)}
{/* Main Card */}
<div className="room-panel rounded-2xl p-6 space-y-6">
{/* User Setup */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Your Profile</h3>
<div>
<label className="block text-sm text-white/60 mb-2">Your Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
className="input w-full"
maxLength={20}
/>
</div>
<div>
<label className="block text-sm text-white/60 mb-2">Your Avatar</label>
<div className="flex gap-2 flex-wrap">
{EMOJI_OPTIONS.map((e) => (
<button
key={e}
onClick={() => setEmoji(e)}
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition-all ${
emoji === e
? 'bg-rmaps-primary scale-110'
: 'bg-white/10 hover:bg-white/20'
}`}
>
{e}
</button>
))}
</div>
</div>
</div>
<hr className="border-white/10" />
{/* Create Room */}
{!isCreating ? (
<div className="space-y-4">
<button
onClick={() => setIsCreating(true)}
className="btn-primary w-full text-lg py-3"
disabled={!name.trim() || !isAuthenticated}
title={!isAuthenticated ? 'Sign in with EncryptID to create rooms' : ''}
>
{isAuthenticated ? 'Create New Map' : 'Sign in to Create Map'}
</button>
<div className="text-center text-white/40 text-sm">or</div>
{/* Join Room */}
<div className="space-y-3">
<input
type="text"
value={joinSlug}
onChange={(e) => setJoinSlug(e.target.value)}
placeholder="Enter room name or code"
className="input w-full"
/>
<button
onClick={handleJoinRoom}
className="btn-secondary w-full"
disabled={!name.trim() || !joinSlug.trim()}
>
Join Existing Map
</button>
</div>
</div>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm text-white/60 mb-2">
Room Name (optional)
</label>
<div className="flex items-center gap-2">
<input
type="text"
value={roomName}
onChange={(e) => setRoomName(e.target.value)}
placeholder="e.g., 39c3-crew"
className="input flex-1"
maxLength={20}
/>
<span className="text-white/40">.rmaps.online</span>
</div>
<p className="text-xs text-white/40 mt-1">
Leave blank for a random code
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setIsCreating(false)}
className="btn-ghost flex-1"
>
Cancel
</button>
<button
onClick={handleCreateRoom}
className="btn-primary flex-1"
disabled={!name.trim()}
>
Create Map
</button>
</div>
</div>
)}
</div>
</div>
</main>
</section>
{/* ── Ecosystem Footer ─────────────────────────────────────── */}
<footer className="border-t border-slate-700/50 py-8">
<div className="max-w-7xl mx-auto px-6">
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-500 mb-4">
<span className="font-medium text-slate-400">r* Ecosystem</span>
<a href="https://rspace.online" className="hover:text-slate-300 transition-colors">rSpace</a>
<a href="https://rmaps.online" className="hover:text-slate-300 transition-colors font-medium text-slate-300">rMaps</a>
<a href="https://rnotes.online" className="hover:text-slate-300 transition-colors">rNotes</a>
<a href="https://rvote.online" className="hover:text-slate-300 transition-colors">rVote</a>
<a href="https://rfunds.online" className="hover:text-slate-300 transition-colors">rFunds</a>
<a href="https://rtrips.online" className="hover:text-slate-300 transition-colors">rTrips</a>
<a href="https://rcart.online" className="hover:text-slate-300 transition-colors">rCart</a>
<a href="https://rwallet.online" className="hover:text-slate-300 transition-colors">rWallet</a>
<a href="https://rfiles.online" className="hover:text-slate-300 transition-colors">rFiles</a>
<a href="https://rtube.online" className="hover:text-slate-300 transition-colors">rTube</a>
<a href="https://rcal.online" className="hover:text-slate-300 transition-colors">rCal</a>
<a href="https://rnetwork.online" className="hover:text-slate-300 transition-colors">rNetwork</a>
<a href="https://rinbox.online" className="hover:text-slate-300 transition-colors">rInbox</a>
<a href="https://rstack.online" className="hover:text-slate-300 transition-colors">rStack</a>
<a href="https://rauctions.online" className="hover:text-slate-300 transition-colors">rAuctions</a>
<a href="https://rpubs.online" className="hover:text-slate-300 transition-colors">rPubs</a>
</div>
<p className="text-center text-xs text-slate-600">
Part of the r* ecosystem collaborative tools for communities.
</p>
</div>
</footer>
</div>
);
}