rmaps-online/src/app/page.tsx

257 lines
8.3 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
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;
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 (
<main className="min-h-screen flex flex-col items-center justify-center p-4">
<div className="max-w-md w-full space-y-8">
{/* Logo/Title */}
<div className="text-center">
<h1 className="text-5xl font-bold mb-2">
<span className="text-rmaps-primary">r</span>Maps
</h1>
<p className="text-white/60">Find your friends at events</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">
<h2 className="text-lg font-medium">Your Profile</h2>
<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()}
>
Create New 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>
{/* Footer */}
<div className="text-center text-white/40 text-sm space-y-2">
<p>Privacy-first location sharing</p>
<p>
Built for{' '}
<a
href="https://events.ccc.de/"
target="_blank"
rel="noopener noreferrer"
className="text-rmaps-primary hover:underline"
>
CCC events
</a>
</p>
</div>
</div>
</main>
);
}