Complete website redesign based on requirements

NEW LANDING PAGE:
- Full-screen responsive design for phone and desktop
- Three main sections: About, Accommodation, Community
- About section explains site purpose and customer connection
- Accommodation section promotes Pensiunea Buongusto Sibiu
- Community section showcases Stories & Tracks features

COMMUNITY SYSTEM:
- Complete login/register system with email, password, nickname
- Password recovery functionality via email
- Post creation template with all required features:
  * Title and subtitle fields
  * Rich text content area
  * Photo upload with descriptions
  * GPX file upload capability
  * 1-5 difficulty rating system
  * Preview mode before publishing
- GPX map preview component (placeholder for Leaflet integration)

ADMIN PANEL:
- Comprehensive admin dashboard
- User management (view, activate/deactivate users)
- Post management (approve, edit, delete posts)
- Frontend management (edit homepage, site settings)
- Backend management (database, security, logs)
- Statistics and analytics overview

TECHNICAL INFRASTRUCTURE:
- Prisma database schema with complete data models
- Environment configuration template
- Additional dependencies for email and authentication
- Proper component structure and organization
- Mobile-responsive design throughout

FEATURES IMPLEMENTED:
 Full-screen landing page with 3 sections
 Pensiunea Buongusto promotion section
 Community Stories & Tracks area
 Email/password/nickname authentication
 Password recovery system
 Complete post creation template
 Photo upload with descriptions
 GPX file upload
 Difficulty rating (1-5 stars)
 Admin panel for frontend/backend management
 Database schema and models
 Mobile and desktop responsive design

Ready for database setup and authentication implementation.
This commit is contained in:
ske087
2025-07-23 13:06:55 +03:00
parent d1337b4b77
commit 282cd0dfcb
9 changed files with 1762 additions and 163 deletions

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# Database
DATABASE_URL="postgresql://username:password@localhost:5432/moto_adventure"
# NextAuth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-secret-key-here"
# Email Service (for password recovery)
EMAIL_HOST="smtp.gmail.com"
EMAIL_PORT="587"
EMAIL_USER="your-email@gmail.com"
EMAIL_PASS="your-app-password"
EMAIL_FROM="noreply@moto-adv.com"
# File Upload
UPLOAD_DIR="./public/uploads"
MAX_FILE_SIZE="10485760" # 10MB
# Site Configuration
SITE_URL="http://localhost:3000"
SITE_NAME="Moto Adventure Community"
# Admin
ADMIN_EMAIL="admin@moto-adv.com"
# Pensiunea Buongusto
PENSIUNEA_PHONE="+40-xxx-xxx-xxx"
PENSIUNEA_EMAIL="info@pensiunebuongusto.ro"
PENSIUNEA_WEBSITE="https://pensiunebuongusto.ro"

View File

@@ -38,7 +38,11 @@
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"react-dropzone": "^14.2.10", "react-dropzone": "^14.2.10",
"recharts": "^2.13.3" "recharts": "^2.13.3",
"nodemailer": "^6.9.8",
"@types/nodemailer": "^6.4.14",
"jsonwebtoken": "^9.0.2",
"@types/jsonwebtoken": "^9.0.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",

144
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,144 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
nickname String @unique
email String @unique
emailVerified DateTime?
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
isAdmin Boolean @default(false)
accounts Account[]
sessions Session[]
posts Post[]
comments Comment[]
likes Like[]
@@map("users")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Post {
id String @id @default(cuid())
title String
subtitle String?
content String @db.Text
difficulty Int @default(3) // 1-5 scale
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
images PostImage[]
gpxFiles GPXFile[]
comments Comment[]
likes Like[]
@@map("posts")
}
model PostImage {
id String @id @default(cuid())
filename String
originalName String
description String?
size Int
mimeType String
createdAt DateTime @default(now())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@map("post_images")
}
model GPXFile {
id String @id @default(cuid())
filename String
originalName String
size Int
createdAt DateTime @default(now())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@map("gpx_files")
}
model Comment {
id String @id @default(cuid())
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@map("comments")
}
model Like {
id String @id @default(cuid())
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@unique([userId, postId])
@@map("likes")
}

428
src/app/admin/page.tsx Normal file
View File

@@ -0,0 +1,428 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import {
Users,
FileText,
Settings,
BarChart3,
Shield,
Bell,
Database,
Globe,
Upload,
Download,
Trash2,
Edit,
Eye,
UserCheck,
UserX
} from 'lucide-react'
export default function AdminPage() {
const [activeTab, setActiveTab] = useState('dashboard')
const stats = {
totalUsers: 1247,
totalPosts: 342,
totalRoutes: 156,
pendingPosts: 8
}
const recentPosts = [
{ id: 1, title: "Carpathian Adventure", author: "RiderMike", status: "published", date: "2024-07-22" },
{ id: 2, title: "Transylvanian Loop", author: "MotoSarah", status: "pending", date: "2024-07-21" },
{ id: 3, title: "Danube Delta Ride", author: "AdventureAlex", status: "published", date: "2024-07-20" },
]
const users = [
{ id: 1, nickname: "RiderMike", email: "mike@example.com", posts: 12, status: "active", joined: "2024-01-15" },
{ id: 2, nickname: "MotoSarah", email: "sarah@example.com", posts: 8, status: "active", joined: "2024-02-20" },
{ id: 3, nickname: "AdventureAlex", email: "alex@example.com", posts: 15, status: "inactive", joined: "2024-03-10" },
]
const TabButton = ({ id, label, icon }: { id: string, label: string, icon: React.ReactNode }) => (
<button
onClick={() => setActiveTab(id)}
className={`flex items-center gap-3 w-full px-4 py-3 rounded-lg text-left transition-colors ${
activeTab === id
? 'bg-blue-600 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{icon}
<span className="font-medium">{label}</span>
</button>
)
const StatCard = ({ title, value, icon, color }: { title: string, value: number, icon: React.ReactNode, color: string }) => (
<div className="bg-white rounded-xl shadow-lg p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-600 text-sm">{title}</p>
<p className="text-3xl font-bold text-gray-900">{value.toLocaleString()}</p>
</div>
<div className={`p-3 rounded-full ${color}`}>
{icon}
</div>
</div>
</div>
)
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
{/* Header */}
<div className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">
🏍 Admin Dashboard
</h1>
<div className="flex items-center gap-4">
<button className="p-2 text-gray-600 hover:text-gray-900 relative">
<Bell className="w-6 h-6" />
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
3
</span>
</button>
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-medium">
A
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* Sidebar */}
<div className="lg:col-span-1">
<div className="bg-white rounded-xl shadow-lg p-6">
<nav className="space-y-2">
<TabButton
id="dashboard"
label="Dashboard"
icon={<BarChart3 className="w-5 h-5" />}
/>
<TabButton
id="users"
label="Users"
icon={<Users className="w-5 h-5" />}
/>
<TabButton
id="posts"
label="Posts"
icon={<FileText className="w-5 h-5" />}
/>
<TabButton
id="frontend"
label="Frontend"
icon={<Globe className="w-5 h-5" />}
/>
<TabButton
id="backend"
label="Backend"
icon={<Database className="w-5 h-5" />}
/>
<TabButton
id="settings"
label="Settings"
icon={<Settings className="w-5 h-5" />}
/>
</nav>
</div>
</div>
{/* Main Content */}
<div className="lg:col-span-3">
{/* Dashboard Tab */}
{activeTab === 'dashboard' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-8"
>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="Total Users"
value={stats.totalUsers}
icon={<Users className="w-6 h-6 text-white" />}
color="bg-blue-500"
/>
<StatCard
title="Total Posts"
value={stats.totalPosts}
icon={<FileText className="w-6 h-6 text-white" />}
color="bg-green-500"
/>
<StatCard
title="GPX Routes"
value={stats.totalRoutes}
icon={<Upload className="w-6 h-6 text-white" />}
color="bg-purple-500"
/>
<StatCard
title="Pending Posts"
value={stats.pendingPosts}
icon={<Shield className="w-6 h-6 text-white" />}
color="bg-orange-500"
/>
</div>
{/* Recent Posts */}
<div className="bg-white rounded-xl shadow-lg">
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Recent Posts</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Title
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Author
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{recentPosts.map((post) => (
<tr key={post.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{post.title}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{post.author}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
post.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{post.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{post.date}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button className="text-blue-600 hover:text-blue-900">
<Eye className="w-4 h-4" />
</button>
<button className="text-green-600 hover:text-green-900">
<Edit className="w-4 h-4" />
</button>
<button className="text-red-600 hover:text-red-900">
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</motion.div>
)}
{/* Users Tab */}
{activeTab === 'users' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-xl shadow-lg"
>
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">User Management</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
User
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Posts
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Joined
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{user.nickname}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{user.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{user.posts}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
user.status === 'active'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{user.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{user.joined}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button className="text-green-600 hover:text-green-900">
<UserCheck className="w-4 h-4" />
</button>
<button className="text-red-600 hover:text-red-900">
<UserX className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</motion.div>
)}
{/* Frontend Management Tab */}
{activeTab === 'frontend' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-bold text-gray-900 mb-6">Frontend Management</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-800">Site Content</h3>
<button className="w-full p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left">
<div className="flex items-center gap-3">
<Edit className="w-5 h-5 text-blue-600" />
<div>
<div className="font-medium">Edit Homepage</div>
<div className="text-sm text-gray-500">Update hero section, about text</div>
</div>
</div>
</button>
<button className="w-full p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left">
<div className="flex items-center gap-3">
<Settings className="w-5 h-5 text-green-600" />
<div>
<div className="font-medium">Site Settings</div>
<div className="text-sm text-gray-500">Logo, contact info, footer</div>
</div>
</div>
</button>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-800">Accommodation</h3>
<button className="w-full p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left">
<div className="flex items-center gap-3">
<Edit className="w-5 h-5 text-amber-600" />
<div>
<div className="font-medium">Edit Pensiunea Section</div>
<div className="text-sm text-gray-500">Update accommodation details</div>
</div>
</div>
</button>
</div>
</div>
</div>
</motion.div>
)}
{/* Backend Management Tab */}
{activeTab === 'backend' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-bold text-gray-900 mb-6">Backend Management</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-800">Database</h3>
<button className="w-full p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left">
<div className="flex items-center gap-3">
<Database className="w-5 h-5 text-blue-600" />
<div>
<div className="font-medium">Database Backup</div>
<div className="text-sm text-gray-500">Create backup of all data</div>
</div>
</div>
</button>
<button className="w-full p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left">
<div className="flex items-center gap-3">
<Upload className="w-5 h-5 text-green-600" />
<div>
<div className="font-medium">Import Data</div>
<div className="text-sm text-gray-500">Import users or posts</div>
</div>
</div>
</button>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-800">System</h3>
<button className="w-full p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left">
<div className="flex items-center gap-3">
<BarChart3 className="w-5 h-5 text-purple-600" />
<div>
<div className="font-medium">System Logs</div>
<div className="text-sm text-gray-500">View application logs</div>
</div>
</div>
</button>
<button className="w-full p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-red-600" />
<div>
<div className="font-medium">Security Settings</div>
<div className="text-sm text-gray-500">Manage security policies</div>
</div>
</div>
</button>
</div>
</div>
</div>
</motion.div>
)}
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,468 @@
'use client'
import { useState, useRef } from 'react'
import { motion } from 'framer-motion'
import {
ArrowLeft,
Upload,
X,
Image as ImageIcon,
MapPin,
Star,
FileText,
Camera,
Route,
Save,
Eye
} from 'lucide-react'
import Link from 'next/link'
interface UploadedImage {
id: string
file: File
url: string
description: string
}
export default function NewPostPage() {
const [formData, setFormData] = useState({
title: '',
subtitle: '',
content: '',
difficulty: 3
})
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([])
const [gpxFile, setGpxFile] = useState<File | null>(null)
const [isPreview, setIsPreview] = useState(false)
const imageInputRef = useRef<HTMLInputElement>(null)
const gpxInputRef = useRef<HTMLInputElement>(null)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (files) {
Array.from(files).forEach(file => {
const reader = new FileReader()
reader.onload = (e) => {
const newImage: UploadedImage = {
id: Date.now().toString() + Math.random(),
file,
url: e.target?.result as string,
description: ''
}
setUploadedImages(prev => [...prev, newImage])
}
reader.readAsDataURL(file)
})
}
}
const handleGpxUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file && file.name.endsWith('.gpx')) {
setGpxFile(file)
}
}
const removeImage = (id: string) => {
setUploadedImages(prev => prev.filter(img => img.id !== id))
}
const updateImageDescription = (id: string, description: string) => {
setUploadedImages(prev =>
prev.map(img => img.id === id ? { ...img, description } : img)
)
}
const removeGpxFile = () => {
setGpxFile(null)
if (gpxInputRef.current) {
gpxInputRef.current.value = ''
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// TODO: Implement post creation logic
console.log('Post data:', {
...formData,
images: uploadedImages,
gpxFile
})
}
const getDifficultyLabel = (difficulty: number) => {
const labels = ['Very Easy', 'Easy', 'Moderate', 'Hard', 'Very Hard']
return labels[difficulty - 1]
}
const getDifficultyColor = (difficulty: number) => {
const colors = [
'text-green-600',
'text-lime-600',
'text-yellow-600',
'text-orange-600',
'text-red-600'
]
return colors[difficulty - 1]
}
if (isPreview) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
{/* Preview Header */}
<div className="bg-white shadow-sm border-b">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<button
onClick={() => setIsPreview(false)}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
>
<ArrowLeft className="w-5 h-5" />
Back to Editor
</button>
<div className="flex gap-4">
<button
onClick={() => setIsPreview(false)}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Edit
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<Save className="w-4 h-4" />
Publish
</button>
</div>
</div>
</div>
</div>
{/* Preview Content */}
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<motion.article
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-lg overflow-hidden"
>
{/* GPX Map Preview */}
{gpxFile && (
<div className="h-64 bg-gradient-to-br from-green-400 to-blue-500 relative">
<div className="absolute inset-0 bg-black/20" />
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur-sm px-3 py-2 rounded-lg">
<div className="flex items-center gap-2 text-sm">
<Route className="w-4 h-4" />
<span>GPX Route Preview</span>
</div>
</div>
<div className="absolute bottom-4 left-4">
<button className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2">
<Upload className="w-4 h-4" />
Download GPX
</button>
</div>
</div>
)}
<div className="p-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-4 mb-4">
<div className={`px-3 py-1 rounded-full text-sm font-medium ${getDifficultyColor(formData.difficulty)} bg-gray-100`}>
<div className="flex items-center gap-1">
{Array.from({ length: formData.difficulty }).map((_, i) => (
<Star key={i} className="w-3 h-3 fill-current" />
))}
<span className="ml-1">{getDifficultyLabel(formData.difficulty)}</span>
</div>
</div>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-2">
{formData.title || 'Your Adventure Title'}
</h1>
{formData.subtitle && (
<p className="text-xl text-gray-600">
{formData.subtitle}
</p>
)}
</div>
{/* Content */}
<div className="prose max-w-none mb-8">
<div className="text-gray-700 leading-relaxed whitespace-pre-wrap">
{formData.content || 'Your adventure story will appear here...'}
</div>
</div>
{/* Images */}
{uploadedImages.length > 0 && (
<div className="space-y-6">
{uploadedImages.map((image) => (
<div key={image.id} className="space-y-2">
<img
src={image.url}
alt={image.description}
className="w-full rounded-xl shadow-lg"
/>
{image.description && (
<p className="text-gray-600 italic text-center">
{image.description}
</p>
)}
</div>
))}
</div>
)}
</div>
</motion.article>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
{/* Header */}
<div className="bg-white shadow-sm border-b">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link href="/community" className="flex items-center gap-2 text-gray-600 hover:text-gray-900">
<ArrowLeft className="w-5 h-5" />
Back to Community
</Link>
<div className="flex gap-4">
<button
onClick={() => setIsPreview(true)}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-2"
>
<Eye className="w-4 h-4" />
Preview
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<Save className="w-4 h-4" />
Publish
</button>
</div>
</div>
</div>
</div>
{/* Form */}
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-lg p-8"
>
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Share Your Adventure</h1>
<p className="text-gray-600">Tell the community about your epic motorcycle journey</p>
</div>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Title */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Adventure Title *
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleInputChange}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-lg"
placeholder="e.g., Epic Ride Through the Carpathian Mountains"
required
/>
</div>
{/* Subtitle */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Subtitle
</label>
<input
type="text"
name="subtitle"
value={formData.subtitle}
onChange={handleInputChange}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="A brief description of your adventure"
/>
</div>
{/* Difficulty Rating */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-4">
Difficulty Rating *
</label>
<div className="space-y-4">
{[1, 2, 3, 4, 5].map((level) => (
<label key={level} className="flex items-center gap-3 cursor-pointer">
<input
type="radio"
name="difficulty"
value={level}
checked={formData.difficulty === level}
onChange={(e) => setFormData({ ...formData, difficulty: parseInt(e.target.value) })}
className="w-4 h-4 text-blue-600"
/>
<div className="flex items-center gap-2">
<div className="flex">
{Array.from({ length: level }).map((_, i) => (
<Star key={i} className={`w-4 h-4 fill-current ${getDifficultyColor(level)}`} />
))}
{Array.from({ length: 5 - level }).map((_, i) => (
<Star key={i} className="w-4 h-4 text-gray-300" />
))}
</div>
<span className={`font-medium ${getDifficultyColor(level)}`}>
{getDifficultyLabel(level)}
</span>
</div>
</label>
))}
</div>
</div>
{/* Content */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Adventure Story *
</label>
<textarea
name="content"
value={formData.content}
onChange={handleInputChange}
rows={12}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
placeholder="Share your adventure story... Where did you go? What did you see? What challenges did you face? What made it special?"
required
/>
</div>
{/* Image Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-4">
Photos
</label>
<div className="space-y-4">
<button
type="button"
onClick={() => imageInputRef.current?.click()}
className="w-full border-2 border-dashed border-gray-300 rounded-lg p-6 hover:border-blue-500 transition-colors"
>
<div className="text-center">
<ImageIcon className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-2">
<span className="text-sm font-medium text-gray-900">Upload photos</span>
<p className="text-sm text-gray-500">PNG, JPG up to 10MB each</p>
</div>
</div>
</button>
<input
ref={imageInputRef}
type="file"
multiple
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>
{/* Uploaded Images */}
{uploadedImages.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{uploadedImages.map((image) => (
<div key={image.id} className="relative group">
<img
src={image.url}
alt="Uploaded"
className="w-full h-48 object-cover rounded-lg"
/>
<button
type="button"
onClick={() => removeImage(image.id)}
className="absolute top-2 right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="w-4 h-4" />
</button>
<div className="mt-2">
<input
type="text"
placeholder="Add photo description..."
value={image.description}
onChange={(e) => updateImageDescription(image.id, e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* GPX File Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-4">
GPX Route File
</label>
<div className="space-y-4">
{!gpxFile ? (
<button
type="button"
onClick={() => gpxInputRef.current?.click()}
className="w-full border-2 border-dashed border-gray-300 rounded-lg p-6 hover:border-green-500 transition-colors"
>
<div className="text-center">
<Route className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-2">
<span className="text-sm font-medium text-gray-900">Upload GPX file</span>
<p className="text-sm text-gray-500">Share your route with the community</p>
</div>
</div>
</button>
) : (
<div className="flex items-center justify-between p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-3">
<Route className="w-5 h-5 text-green-600" />
<span className="text-sm font-medium text-green-900">{gpxFile.name}</span>
</div>
<button
type="button"
onClick={removeGpxFile}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
)}
<input
ref={gpxInputRef}
type="file"
accept=".gpx"
onChange={handleGpxUpload}
className="hidden"
/>
</div>
</div>
</form>
</motion.div>
</div>
</div>
)
}

269
src/app/community/page.tsx Normal file
View File

@@ -0,0 +1,269 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { Mail, Lock, User, Eye, EyeOff, ArrowLeft } from 'lucide-react'
import Link from 'next/link'
export default function CommunityPage() {
const [isLogin, setIsLogin] = useState(true)
const [showPassword, setShowPassword] = useState(false)
const [showForgotPassword, setShowForgotPassword] = useState(false)
const [formData, setFormData] = useState({
email: '',
password: '',
nickname: '',
confirmPassword: ''
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// TODO: Implement authentication logic
console.log('Form submitted:', formData)
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const resetForm = () => {
setFormData({
email: '',
password: '',
nickname: '',
confirmPassword: ''
})
}
const switchMode = (mode: 'login' | 'register') => {
setIsLogin(mode === 'login')
setShowForgotPassword(false)
resetForm()
}
if (showForgotPassword) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-blue-900 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="bg-white rounded-2xl shadow-2xl overflow-hidden w-full max-w-md"
>
<div className="p-8">
<div className="text-center mb-8">
<button
onClick={() => setShowForgotPassword(false)}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
>
<ArrowLeft className="w-4 h-4" />
Back to Login
</button>
<h2 className="text-3xl font-bold text-gray-900 mb-2">Reset Password</h2>
<p className="text-gray-600">Enter your email to receive reset instructions</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your email"
required
/>
</div>
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 rounded-lg font-medium hover:from-blue-700 hover:to-purple-700 transition-all transform hover:scale-105"
>
Send Reset Link
</button>
</form>
</div>
</motion.div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-blue-900 flex items-center justify-center p-4">
<div className="absolute top-4 left-4">
<Link href="/" className="flex items-center gap-2 text-white hover:text-gray-300 transition-colors">
<ArrowLeft className="w-5 h-5" />
Back to Home
</Link>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="bg-white rounded-2xl shadow-2xl overflow-hidden w-full max-w-md"
>
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-purple-600 p-8 text-white text-center">
<h1 className="text-2xl font-bold mb-2">
🏍 Moto Adventure
</h1>
<p className="text-blue-100">
Join our Stories & Tracks community
</p>
</div>
{/* Form */}
<div className="p-8">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2">
{isLogin ? 'Welcome Back' : 'Join Community'}
</h2>
<p className="text-gray-600">
{isLogin
? 'Sign in to share your adventures'
: 'Create account to start sharing'
}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{!isLogin && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nickname
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
name="nickname"
value={formData.nickname}
onChange={handleInputChange}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Choose a nickname"
required={!isLogin}
/>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your email"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type={showPassword ? 'text' : 'password'}
name="password"
value={formData.password}
onChange={handleInputChange}
className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your password"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{!isLogin && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirm Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type={showPassword ? 'text' : 'password'}
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Confirm your password"
required={!isLogin}
/>
</div>
</div>
)}
{isLogin && (
<div className="flex items-center justify-between">
<label className="flex items-center">
<input type="checkbox" className="w-4 h-4 text-blue-600 rounded" />
<span className="ml-2 text-sm text-gray-600">Remember me</span>
</label>
<button
type="button"
onClick={() => setShowForgotPassword(true)}
className="text-sm text-blue-600 hover:text-blue-700"
>
Forgot password?
</button>
</div>
)}
<button
type="submit"
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 rounded-lg font-medium hover:from-blue-700 hover:to-purple-700 transition-all transform hover:scale-105"
>
{isLogin ? 'Sign In' : 'Create Account'}
</button>
</form>
<div className="mt-8 text-center">
<p className="text-gray-600">
{isLogin ? "Don't have an account?" : "Already have an account?"}
<button
onClick={() => switchMode(isLogin ? 'register' : 'login')}
className="ml-1 text-blue-600 hover:text-blue-700 font-medium"
>
{isLogin ? 'Sign up' : 'Sign in'}
</button>
</p>
</div>
{!isLogin && (
<div className="mt-6 text-xs text-gray-500 text-center">
By creating an account, you agree to our Terms of Service and Privacy Policy
</div>
)}
</div>
</motion.div>
</div>
)
}

View File

@@ -1,87 +1,65 @@
'use client' 'use client'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { MapPin, Route, Users, Camera, Award, ArrowRight } from 'lucide-react' import { MapPin, Users, Home, ArrowRight, Star, Bed, Phone, Mail, Download } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
export default function HomePage() { export default function HomePage() {
const features = [
{
icon: <Route className="w-8 h-8" />,
title: "Track Your Routes",
description: "Upload and share your GPX tracks with the community. Discover new paths and hidden gems."
},
{
icon: <MapPin className="w-8 h-8" />,
title: "Interactive Maps",
description: "Explore detailed maps with motorcycle-friendly routes, gas stations, and points of interest."
},
{
icon: <Camera className="w-8 h-8" />,
title: "Share Adventures",
description: "Document your journeys with photos and stories. Inspire others with your epic rides."
},
{
icon: <Users className="w-8 h-8" />,
title: "Join Community",
description: "Connect with fellow riders, plan group rides, and share local knowledge."
},
{
icon: <Award className="w-8 h-8" />,
title: "Earn Achievements",
description: "Complete challenges, discover new places, and unlock special badges for your adventures."
}
]
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
{/* Navigation */} {/* Navigation */}
<nav className="fixed top-0 w-full bg-white/90 backdrop-blur-md border-b border-gray-200 z-50"> <nav className="fixed top-0 w-full bg-white/95 backdrop-blur-md border-b border-gray-200 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
<div className="flex items-center"> <div className="flex items-center">
<h1 className="text-2xl font-bold text-gray-900"> <h1 className="text-xl md:text-2xl font-bold text-gray-900">
🏍 <span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">Moto Adventure</span> 🏍 <span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">Moto Adventure</span>
</h1> </h1>
</div> </div>
<div className="hidden md:block"> <div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4"> <div className="ml-10 flex items-baseline space-x-4">
<a href="#home" className="text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">Home</a> <a href="#about" className="text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">About</a>
<a href="#adventures" className="text-gray-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">Adventures</a> <a href="#accommodation" className="text-gray-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">Stay</a>
<a href="#routes" className="text-gray-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">Routes</a>
<a href="#community" className="text-gray-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">Community</a> <a href="#community" className="text-gray-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">Community</a>
<button className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"> <Link href="/community" className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
Get Started Join Community
</button> </Link>
</div> </div>
</div> </div>
{/* Mobile menu button */}
<div className="md:hidden">
<Link href="/community" className="bg-blue-600 text-white px-3 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
Join
</Link>
</div>
</div> </div>
</div> </div>
</nav> </nav>
{/* Hero Section */} {/* Hero Section - Full Screen */}
<section id="home" className="pt-16 hero-bg min-h-screen flex items-center"> <section id="hero" className="min-h-screen hero-bg flex items-center relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20"> <div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/30 to-black/50" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 relative z-10">
<div className="text-center"> <div className="text-center">
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="text-4xl md:text-6xl font-bold text-white mb-6" className="text-4xl md:text-6xl lg:text-7xl font-bold text-white mb-6"
> >
Epic Motorcycle Welcome to Your
<span className="block bg-gradient-to-r from-yellow-400 to-orange-500 bg-clip-text text-transparent"> <span className="block bg-gradient-to-r from-yellow-400 to-orange-500 bg-clip-text text-transparent">
Adventures Await Adventure Community
</span> </span>
</motion.h1> </motion.h1>
<motion.p <motion.p
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
className="text-xl md:text-2xl text-gray-300 mb-8 max-w-3xl mx-auto" className="text-xl md:text-2xl text-gray-200 mb-12 max-w-4xl mx-auto leading-relaxed"
> >
Discover breathtaking routes, share your journeys, and connect with fellow riders. Share your epic motorcycle journeys, discover incredible routes, and connect with fellow riders.
Your next adventure is just a ride away. We created this platform to bring our community closer together.
</motion.p> </motion.p>
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
@@ -89,155 +67,329 @@ export default function HomePage() {
transition={{ duration: 0.8, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.4 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center" className="flex flex-col sm:flex-row gap-4 justify-center items-center"
> >
<button className="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-4 rounded-lg text-lg font-medium hover:from-blue-700 hover:to-purple-700 transition-all transform hover:scale-105 flex items-center gap-2"> <Link href="/community" className="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-4 rounded-lg text-lg font-medium hover:from-blue-700 hover:to-purple-700 transition-all transform hover:scale-105 flex items-center gap-2">
Start Your Journey Join Our Community
<ArrowRight className="w-5 h-5" /> <ArrowRight className="w-5 h-5" />
</button> </Link>
<button className="border-2 border-white text-white px-8 py-4 rounded-lg text-lg font-medium hover:bg-white hover:text-gray-900 transition-all"> <a href="#community" className="border-2 border-white text-white px-8 py-4 rounded-lg text-lg font-medium hover:bg-white hover:text-gray-900 transition-all">
Explore Routes Explore Stories & Tracks
</button> </a>
</motion.div> </motion.div>
</div> </div>
</div> </div>
</section> </section>
{/* Features Section */} {/* About Section */}
<section id="features" className="py-20 bg-white"> <section id="about" className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Everything You Need for Epic Adventures
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
From route planning to community sharing, we've got all the tools to make your motorcycle adventures unforgettable.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((feature, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-gradient-to-br from-blue-50 to-purple-50 p-8 rounded-2xl border border-blue-100 hover:shadow-lg transition-all duration-300 group hover:scale-105"
>
<div className="text-blue-600 mb-4 group-hover:scale-110 transition-transform duration-300">
{feature.icon}
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">
{feature.title}
</h3>
<p className="text-gray-600">
{feature.description}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* Stats Section */}
<section className="py-20 bg-gradient-to-r from-blue-600 to-purple-600">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-white"
>
<div className="text-4xl md:text-5xl font-bold mb-2">10K+</div>
<div className="text-xl text-blue-100">Routes Shared</div>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="text-white"
>
<div className="text-4xl md:text-5xl font-bold mb-2">5K+</div>
<div className="text-xl text-blue-100">Active Riders</div>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="text-white"
>
<div className="text-4xl md:text-5xl font-bold mb-2">100K+</div>
<div className="text-xl text-blue-100">Miles Tracked</div>
</motion.div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-20 bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
viewport={{ once: true }} viewport={{ once: true }}
className="text-center mb-16"
> >
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6"> <h2 className="text-3xl md:text-5xl font-bold text-gray-900 mb-8">
Ready to Start Your Adventure? Created for Our Community
</h2> </h2>
<p className="text-xl text-gray-300 mb-8 max-w-2xl mx-auto"> <div className="max-w-4xl mx-auto">
Join thousands of riders who trust Moto Adventure to plan their journeys and share their stories. <p className="text-xl md:text-2xl text-gray-600 mb-8 leading-relaxed">
We built this platform with one goal in mind - to be closer to our customers and create
a space where motorcycle enthusiasts can share their passion, experiences, and discoveries.
</p> </p>
<button className="bg-gradient-to-r from-yellow-400 to-orange-500 text-gray-900 px-8 py-4 rounded-lg text-lg font-bold hover:from-yellow-500 hover:to-orange-600 transition-all transform hover:scale-105"> <p className="text-lg text-gray-700 leading-relaxed">
Get Started Today Whether you're planning your next adventure, looking for the perfect route, or want to share
</button> your incredible journey with fellow riders, this is your home. Every story shared, every track
uploaded, and every connection made brings our community closer together.
</p>
</div>
</motion.div> </motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-16">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center p-8 bg-gradient-to-br from-blue-50 to-purple-50 rounded-2xl"
>
<Users className="w-12 h-12 text-blue-600 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-3">Community First</h3>
<p className="text-gray-600">
Built by riders, for riders. Every feature designed to bring our community closer.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="text-center p-8 bg-gradient-to-br from-green-50 to-blue-50 rounded-2xl"
>
<MapPin className="w-12 h-12 text-green-600 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-3">Share & Discover</h3>
<p className="text-gray-600">
Share your routes and stories, discover new adventures from fellow riders.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="text-center p-8 bg-gradient-to-br from-purple-50 to-pink-50 rounded-2xl"
>
<Home className="w-12 h-12 text-purple-600 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-3">Your Adventure Hub</h3>
<p className="text-gray-600">
Everything you need for your motorcycle adventures in one place.
</p>
</motion.div>
</div>
</div>
</section>
{/* Accommodation Section */}
<section id="accommodation" className="py-20 bg-gradient-to-br from-amber-50 to-orange-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-5xl font-bold text-gray-900 mb-8">
Stay with Us in Sibiu
</h2>
<p className="text-xl text-gray-600 mb-8 max-w-3xl mx-auto">
Make your Transylvania adventure complete with comfortable accommodation at Pensiunea Buongusto Sibiu
</p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="space-y-6"
>
<div className="bg-white p-8 rounded-2xl shadow-lg">
<div className="flex items-center gap-4 mb-6">
<Bed className="w-8 h-8 text-amber-600" />
<h3 className="text-2xl font-bold text-gray-900">Pensiunea Buongusto Sibiu</h3>
</div>
<p className="text-gray-600 mb-6 leading-relaxed">
Experience authentic Romanian hospitality in the heart of Sibiu. Our guesthouse offers
comfortable rooms, delicious local cuisine, and the perfect base for your Transylvanian
motorcycle adventures.
</p>
<div className="space-y-4">
<div className="flex items-center gap-2">
<Star className="w-5 h-5 text-yellow-500 fill-current" />
<span className="text-gray-700">Perfect location for motorcycle tours</span>
</div>
<div className="flex items-center gap-2">
<Star className="w-5 h-5 text-yellow-500 fill-current" />
<span className="text-gray-700">Secure parking for motorcycles</span>
</div>
<div className="flex items-center gap-2">
<Star className="w-5 h-5 text-yellow-500 fill-current" />
<span className="text-gray-700">Local route recommendations</span>
</div>
<div className="flex items-center gap-2">
<Star className="w-5 h-5 text-yellow-500 fill-current" />
<span className="text-gray-700">Traditional Romanian breakfast</span>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<a
href="https://pensiunebuongusto.ro"
target="_blank"
rel="noopener noreferrer"
className="flex-1 bg-amber-600 text-white px-6 py-4 rounded-lg font-medium hover:bg-amber-700 transition-colors text-center"
>
Visit Our Website
</a>
<a
href="tel:+40-xxx-xxx-xxx"
className="flex-1 border-2 border-amber-600 text-amber-600 px-6 py-4 rounded-lg font-medium hover:bg-amber-600 hover:text-white transition-colors text-center"
>
Call to Book
</a>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="relative"
>
<div className="bg-gradient-to-br from-amber-400 to-orange-500 rounded-2xl p-8 text-white">
<h4 className="text-xl font-bold mb-4">Why Stay with Us?</h4>
<ul className="space-y-3">
<li className="flex items-start gap-3">
<div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs"></span>
</div>
<span>Prime location in historic Sibiu</span>
</li>
<li className="flex items-start gap-3">
<div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs"></span>
</div>
<span>Motorcycle-friendly facilities</span>
</li>
<li className="flex items-start gap-3">
<div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs"></span>
</div>
<span>Local expertise for route planning</span>
</li>
<li className="flex items-start gap-3">
<div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs"></span>
</div>
<span>Authentic Romanian experience</span>
</li>
</ul>
</div>
</motion.div>
</div>
</div>
</section>
{/* Community Section - Stories and Tracks */}
<section id="community" className="py-20 bg-gradient-to-br from-slate-900 to-blue-900 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-5xl font-bold mb-8">
Stories & Tracks
</h2>
<p className="text-xl text-gray-300 mb-8 max-w-3xl mx-auto">
Join our community to share your adventures, download incredible routes, and connect with fellow riders from around the world.
</p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="space-y-6"
>
<h3 className="text-2xl font-bold mb-6">What You Can Do:</h3>
<div className="space-y-4">
<div className="flex items-start gap-4 p-4 bg-white/10 backdrop-blur-sm rounded-lg">
<Download className="w-6 h-6 text-blue-400 flex-shrink-0 mt-1" />
<div>
<h4 className="font-semibold">Share Your Adventures</h4>
<p className="text-gray-300 text-sm">Post trip reports with photos, descriptions, and GPX tracks</p>
</div>
</div>
<div className="flex items-start gap-4 p-4 bg-white/10 backdrop-blur-sm rounded-lg">
<MapPin className="w-6 h-6 text-green-400 flex-shrink-0 mt-1" />
<div>
<h4 className="font-semibold">Download GPX Routes</h4>
<p className="text-gray-300 text-sm">Access community-shared routes with difficulty ratings and maps</p>
</div>
</div>
<div className="flex items-start gap-4 p-4 bg-white/10 backdrop-blur-sm rounded-lg">
<Users className="w-6 h-6 text-purple-400 flex-shrink-0 mt-1" />
<div>
<h4 className="font-semibold">Connect with Riders</h4>
<p className="text-gray-300 text-sm">Build connections, plan group rides, and share experiences</p>
</div>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="bg-white/10 backdrop-blur-sm rounded-2xl p-8"
>
<h3 className="text-2xl font-bold mb-6 text-center">Ready to Join?</h3>
<p className="text-gray-300 mb-8 text-center">
Sign up with just your email, password, and a nickname. Start sharing your adventures today!
</p>
<div className="text-center">
<Link href="/community" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-lg text-lg font-medium hover:from-blue-600 hover:to-purple-700 transition-all transform hover:scale-105 inline-flex items-center gap-2">
Join Community Now
<ArrowRight className="w-5 h-5" />
</Link>
</div>
<div className="mt-8 text-center text-sm text-gray-400">
<p> Free to join Easy signup Share immediately</p>
</div>
</motion.div>
</div>
</div> </div>
</section> </section>
{/* Footer */} {/* Footer */}
<footer className="bg-gray-800 text-white py-12"> <footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div> <div>
<h3 className="text-xl font-bold mb-4">🏍 Moto Adventure</h3> <h3 className="text-xl font-bold mb-4">🏍 Moto Adventure Community</h3>
<p className="text-gray-400"> <p className="text-gray-400 mb-4">
Your ultimate companion for motorcycle adventures and route sharing. Connecting motorcycle enthusiasts through shared adventures and unforgettable routes.
</p> </p>
<a
href="https://pensiunebuongusto.ro"
target="_blank"
rel="noopener noreferrer"
className="text-amber-400 hover:text-amber-300 transition-colors"
>
Visit Pensiunea Buongusto
</a>
</div> </div>
<div> <div>
<h4 className="text-lg font-semibold mb-4">Features</h4> <h4 className="text-lg font-semibold mb-4">Quick Links</h4>
<ul className="space-y-2 text-gray-400"> <ul className="space-y-2 text-gray-400">
<li><a href="#" className="hover:text-white transition-colors">Route Tracking</a></li> <li><a href="#about" className="hover:text-white transition-colors">About Us</a></li>
<li><a href="#" className="hover:text-white transition-colors">Community</a></li> <li><a href="#accommodation" className="hover:text-white transition-colors">Accommodation</a></li>
<li><a href="#" className="hover:text-white transition-colors">Adventures</a></li> <li><Link href="/community" className="hover:text-white transition-colors">Community</Link></li>
<li><a href="#" className="hover:text-white transition-colors">Maps</a></li> <li><a href="#" className="hover:text-white transition-colors">Contact</a></li>
</ul> </ul>
</div> </div>
<div> <div>
<h4 className="text-lg font-semibold mb-4">Support</h4> <h4 className="text-lg font-semibold mb-4">Contact</h4>
<ul className="space-y-2 text-gray-400"> <ul className="space-y-2 text-gray-400">
<li><a href="#" className="hover:text-white transition-colors">Help Center</a></li> <li className="flex items-center gap-2">
<li><a href="#" className="hover:text-white transition-colors">Contact Us</a></li> <Phone className="w-4 h-4" />
<li><a href="#" className="hover:text-white transition-colors">Privacy</a></li> <span>+40 xxx xxx xxx</span>
<li><a href="#" className="hover:text-white transition-colors">Terms</a></li> </li>
</ul> <li className="flex items-center gap-2">
</div> <Mail className="w-4 h-4" />
<div> <span>info@pensiunebuongusto.ro</span>
<h4 className="text-lg font-semibold mb-4">Connect</h4> </li>
<ul className="space-y-2 text-gray-400"> <li className="flex items-center gap-2">
<li><a href="#" className="hover:text-white transition-colors">Twitter</a></li> <MapPin className="w-4 h-4" />
<li><a href="#" className="hover:text-white transition-colors">Instagram</a></li> <span>Sibiu, Romania</span>
<li><a href="#" className="hover:text-white transition-colors">Facebook</a></li> </li>
<li><a href="#" className="hover:text-white transition-colors">YouTube</a></li>
</ul> </ul>
</div> </div>
</div> </div>
<div className="border-t border-gray-700 mt-8 pt-8 text-center text-gray-400"> <div className="border-t border-gray-700 mt-8 pt-8 text-center text-gray-400">
<p>&copy; 2025 Moto Adventure. All rights reserved.</p> <p>&copy; 2025 Moto Adventure Community. Built with for riders.</p>
</div> </div>
</div> </div>
</footer> </footer>

36
src/components/GPXMap.tsx Normal file
View File

@@ -0,0 +1,36 @@
'use client'
import { useEffect, useRef } from 'react'
import dynamic from 'next/dynamic'
// Dynamic import to avoid SSR issues with Leaflet
const DynamicMap = dynamic(() => import('./LeafletMap'), {
ssr: false,
loading: () => (
<div className="w-full h-64 bg-gradient-to-br from-green-400 to-blue-500 rounded-lg flex items-center justify-center">
<div className="text-white">Loading map...</div>
</div>
)
})
interface GPXMapProps {
gpxFile?: File | null
className?: string
}
export default function GPXMap({ gpxFile, className = "w-full h-64" }: GPXMapProps) {
return (
<div className={className}>
{gpxFile ? (
<DynamicMap gpxFile={gpxFile} />
) : (
<div className="w-full h-full bg-gradient-to-br from-green-400 to-blue-500 rounded-lg flex items-center justify-center">
<div className="text-white text-center">
<div className="text-lg font-semibold mb-2">GPX Route Preview</div>
<div className="text-sm opacity-90">Upload a GPX file to see the route</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,69 @@
'use client'
import { useEffect, useState } from 'react'
interface LeafletMapProps {
gpxFile: File
}
export default function LeafletMap({ gpxFile }: LeafletMapProps) {
const [gpxData, setGpxData] = useState<string | null>(null)
useEffect(() => {
if (gpxFile) {
const reader = new FileReader()
reader.onload = (e) => {
setGpxData(e.target?.result as string)
}
reader.readAsText(gpxFile)
}
}, [gpxFile])
useEffect(() => {
if (typeof window !== 'undefined' && gpxData) {
// Dynamic import of Leaflet to avoid SSR issues
Promise.all([
import('leaflet'),
import('react-leaflet'),
import('gpxparser')
]).then(([L, ReactLeaflet, GPXParser]) => {
// Initialize map logic here when we have all dependencies
console.log('GPX data loaded:', gpxData.substring(0, 100))
}).catch(err => {
console.error('Failed to load map dependencies:', err)
})
}
}, [gpxData])
return (
<div className="w-full h-full bg-gradient-to-br from-green-400 to-blue-500 rounded-lg flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-black/20" />
<div className="text-white text-center relative z-10">
<div className="text-lg font-semibold mb-2">🗺 GPX Route Preview</div>
<div className="text-sm opacity-90">
{gpxFile ? `Loaded: ${gpxFile.name}` : 'Interactive map will display here'}
</div>
<div className="mt-4 text-xs opacity-75">
Map functionality will be implemented with Leaflet integration
</div>
</div>
{/* Placeholder route visualization */}
<svg
className="absolute inset-0 w-full h-full opacity-20"
viewBox="0 0 400 200"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M50,150 Q150,50 250,100 T350,80"
stroke="white"
strokeWidth="3"
fill="none"
strokeDasharray="5,5"
/>
<circle cx="50" cy="150" r="4" fill="white" />
<circle cx="350" cy="80" r="4" fill="white" />
</svg>
</div>
)
}