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",
"sharp": "^0.33.5",
"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": {
"@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'
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'
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 (
<div className="min-h-screen">
{/* 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="flex justify-between items-center h-16">
<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>
</h1>
</div>
<div className="hidden md:block">
<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="#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="#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="#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="#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="#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">
Get Started
</button>
<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">
Join Community
</Link>
</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>
</nav>
{/* Hero Section */}
<section id="home" className="pt-16 hero-bg min-h-screen flex items-center">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
{/* Hero Section - Full Screen */}
<section id="hero" className="min-h-screen hero-bg flex items-center relative">
<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">
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
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">
Adventures Await
Adventure Community
</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
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.
Your next adventure is just a ride away.
Share your epic motorcycle journeys, discover incredible routes, and connect with fellow riders.
We created this platform to bring our community closer together.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
@@ -89,155 +67,329 @@ export default function HomePage() {
transition={{ duration: 0.8, delay: 0.4 }}
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">
Start Your Journey
<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">
Join Our Community
<ArrowRight className="w-5 h-5" />
</button>
<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">
Explore Routes
</button>
</Link>
<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 Stories & Tracks
</a>
</motion.div>
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="py-20 bg-white">
{/* About Section */}
<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="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
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-4xl font-bold text-white mb-6">
Ready to Start Your Adventure?
<h2 className="text-3xl md:text-5xl font-bold text-gray-900 mb-8">
Created for Our Community
</h2>
<p className="text-xl text-gray-300 mb-8 max-w-2xl mx-auto">
Join thousands of riders who trust Moto Adventure to plan their journeys and share their stories.
</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">
Get Started Today
</button>
<div className="max-w-4xl mx-auto">
<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 className="text-lg text-gray-700 leading-relaxed">
Whether you're planning your next adventure, looking for the perfect route, or want to share
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>
<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>
</section>
{/* 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="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 className="text-xl font-bold mb-4">🏍 Moto Adventure</h3>
<p className="text-gray-400">
Your ultimate companion for motorcycle adventures and route sharing.
<h3 className="text-xl font-bold mb-4">🏍 Moto Adventure Community</h3>
<p className="text-gray-400 mb-4">
Connecting motorcycle enthusiasts through shared adventures and unforgettable routes.
</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>
<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">
<li><a href="#" className="hover:text-white transition-colors">Route Tracking</a></li>
<li><a href="#" className="hover:text-white transition-colors">Community</a></li>
<li><a href="#" className="hover:text-white transition-colors">Adventures</a></li>
<li><a href="#" className="hover:text-white transition-colors">Maps</a></li>
<li><a href="#about" className="hover:text-white transition-colors">About Us</a></li>
<li><a href="#accommodation" className="hover:text-white transition-colors">Accommodation</a></li>
<li><Link href="/community" className="hover:text-white transition-colors">Community</Link></li>
<li><a href="#" className="hover:text-white transition-colors">Contact</a></li>
</ul>
</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">
<li><a href="#" className="hover:text-white transition-colors">Help Center</a></li>
<li><a href="#" className="hover:text-white transition-colors">Contact Us</a></li>
<li><a href="#" className="hover:text-white transition-colors">Privacy</a></li>
<li><a href="#" className="hover:text-white transition-colors">Terms</a></li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Connect</h4>
<ul className="space-y-2 text-gray-400">
<li><a href="#" className="hover:text-white transition-colors">Twitter</a></li>
<li><a href="#" className="hover:text-white transition-colors">Instagram</a></li>
<li><a href="#" className="hover:text-white transition-colors">Facebook</a></li>
<li><a href="#" className="hover:text-white transition-colors">YouTube</a></li>
<li className="flex items-center gap-2">
<Phone className="w-4 h-4" />
<span>+40 xxx xxx xxx</span>
</li>
<li className="flex items-center gap-2">
<Mail className="w-4 h-4" />
<span>info@pensiunebuongusto.ro</span>
</li>
<li className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span>Sibiu, Romania</span>
</li>
</ul>
</div>
</div>
<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>
</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>
)
}