rmaps-online/src/app/page.tsx

424 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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
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);
}
} catch {
// Ignore parse errors
}
// Set random emoji if none loaded
if (!loadedEmoji) {
setEmoji(EMOJI_OPTIONS[Math.floor(Math.random() * EMOJI_OPTIONS.length)]);
}
setIsLoaded(true);
}, []);
// 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">
{/* ── 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://rnetwork.online" className="hover:text-slate-300 transition-colors">🌐 rNetwork</a>
</div>
<p className="text-center text-xs text-slate-600">
Part of the r* ecosystem collaborative tools for communities.
</p>
</div>
</footer>
</div>
);
}