Migrate from Next.js to Flask: Complete motorcycle adventure community website

- Replace Next.js/React implementation with Python Flask
- Add colorful blue-purple-teal gradient theme replacing red design
- Integrate logo and Transalpina panoramic background image
- Implement complete authentication system with Flask-Login
- Add community features for stories and tracks sharing
- Create responsive design with Tailwind CSS
- Add error handling with custom 404/500 pages
- Include Docker deployment configuration
- Add favicon support and proper SEO structure
- Update content for Pensiune BuonGusto accommodation
- Remove deprecated Next.js files and dependencies

Features:
 Landing page with hero section and featured content
 User registration and login system
 Community section for adventure sharing
 Admin panel for content management
 Responsive mobile-first design
 Docker containerization with PostgreSQL
 Email integration with Flask-Mail
 Form validation with WTForms
 SQLAlchemy database models
 Error pages and favicon handling
This commit is contained in:
ske087
2025-07-23 14:42:40 +03:00
parent 282cd0dfcb
commit fc463dc69a
45 changed files with 1572 additions and 11044 deletions

View File

@@ -1,29 +0,0 @@
# 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

@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

80
.gitignore vendored
View File

@@ -1,80 +0,0 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Production builds
.next/
out/
dist/
build/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# Next.js
.next
# Prisma
prisma/migrations/
# Uploads
public/uploads/
# Database
*.db
*.sqlite

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM python:3.11-slim
WORKDIR /opt/site/flask-moto-adventure
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create uploads directory
RUN mkdir -p uploads/images uploads/gpx
# Create non-root user
RUN adduser --disabled-password --gecos '' appuser && chown -R appuser:appuser /opt/site/flask-moto-adventure
USER appuser
# Expose port
EXPOSE 5000
# Run the application with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "120", "run:app"]

171
README.md
View File

@@ -1,62 +1,159 @@
# Moto Adventure Website
# Motorcycle Adventure Community
A Next.js-based website for motorcycle adventure enthusiasts, featuring route tracking, blog posts, and community features.
A Flask-based web application for motorcycle enthusiasts featuring adventure stories, route sharing, and accommodation recommendations.
## Features
- 🏍️ Motorcycle adventure route tracking with GPX support
- 📝 Blog system for sharing adventure stories
- 🗺️ Interactive maps with Leaflet integration
- 👤 User authentication with NextAuth.js
- 📊 Analytics and charts with Recharts
- 📱 Responsive design with Tailwind CSS
- 🖼️ Image upload and processing with Sharp
- 🎨 Smooth animations with Framer Motion
- **Landing Page**: Hero section with call-to-action, featured adventures, and accommodation promotion
- **User Authentication**: Registration and login system with Flask-Login
- **Community Section**: Share stories, tracks, and experiences
- **Admin Panel**: Content management for administrators
- **Responsive Design**: Built with Tailwind CSS for mobile-first design
- **Error Handling**: Custom 404 and 500 error pages
## Tech Stack
## Technology Stack
- **Framework:** Next.js 14 with App Router
- **Language:** TypeScript
- **Styling:** Tailwind CSS
- **Database:** Prisma ORM
- **Authentication:** NextAuth.js
- **Maps:** React Leaflet
- **Forms:** React Hook Form
- **Icons:** Lucide React
- **Animation:** Framer Motion
- **Backend**: Python Flask 3.0.0
- **Database**: SQLAlchemy with PostgreSQL support
- **Authentication**: Flask-Login
- **Email**: Flask-Mail
- **Forms**: WTForms with validation
- **Styling**: Tailwind CSS (CDN)
- **Deployment**: Docker with Gunicorn
## Getting Started
## Project Structure
1. Install dependencies:
```
/
├── app/
│ ├── __init__.py # Flask app factory
│ ├── models.py # Database models
│ ├── forms.py # WTForm definitions
│ ├── routes/ # Route blueprints
│ │ ├── main.py # Main routes (index, about)
│ │ ├── auth.py # Authentication routes
│ │ └── community.py # Community features
│ ├── templates/ # Jinja2 templates
│ │ ├── base.html # Base template
│ │ ├── index.html # Landing page
│ │ ├── auth/ # Auth templates
│ │ └── errors/ # Error pages
│ └── static/ # Static assets
│ ├── favicon.ico
│ └── images/
├── config.py # Application configuration
├── requirements.txt # Python dependencies
├── run.py # Application entry point
├── Dockerfile # Docker configuration
└── docker-compose.yml # Docker Compose setup
```
## Quick Start
### Local Development
1. **Clone the repository**
```bash
npm install
git clone <repository-url>
cd motorcycle-adventure-community
```
2. Set up environment variables:
2. **Create virtual environment**
```bash
cp .env.example .env.local
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Set up the database:
3. **Install dependencies**
```bash
npx prisma generate
npx prisma db push
pip install -r requirements.txt
```
4. Run the development server:
4. **Set environment variables**
```bash
npm run dev
export FLASK_APP=run.py
export FLASK_ENV=development
export SECRET_KEY="your-secret-key-here"
export DATABASE_URL="sqlite:///motorcycle_adventures.db"
```
5. Open [http://localhost:3000](http://localhost:3000) in your browser.
5. **Initialize database**
```bash
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
```
## Development
6. **Run the application**
```bash
python run.py
```
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run start` - Start production server
- `npm run lint` - Run ESLint
### Docker Deployment
1. **Build and run with Docker Compose**
```bash
docker-compose up --build
```
2. **Access the application**
- Web application: http://localhost:5000
- PostgreSQL database: localhost:5432
## Configuration
The application supports multiple environments through environment variables:
- `FLASK_ENV`: development, testing, production
- `SECRET_KEY`: Flask secret key for sessions
- `DATABASE_URL`: Database connection string
- `MAIL_SERVER`: SMTP server for email
- `MAIL_USERNAME`: Email username
- `MAIL_PASSWORD`: Email password
## Features Overview
### Landing Page
- Hero section with stunning mountain panorama background
- Call-to-action for adventure community
- Featured adventures showcase
- Pensiune BuonGusto accommodation promotion
### Authentication System
- User registration with email validation
- Secure login/logout functionality
- Password hashing with Werkzeug
- Session management with Flask-Login
### Community Features
- Story and track sharing
- User profiles
- Image uploads for adventures
- GPX file support for route sharing
- Comment system
- Like/rating system
### Admin Panel
- User management
- Content moderation
- Analytics dashboard
- System configuration
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is private and proprietary.
This project is licensed under the MIT License - see the LICENSE file for details.
## Acknowledgments
- Beautiful Transalpina panorama background image
- Tailwind CSS for responsive design
- Flask community for excellent documentation
- Bootstrap icons for UI elements

60
app/__init__.py Normal file
View File

@@ -0,0 +1,60 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
from config import config
import os
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
mail = Mail()
def create_app(config_name=None):
app = Flask(__name__)
config_name = config_name or os.environ.get('FLASK_CONFIG') or 'default'
app.config.from_object(config[config_name])
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
mail.init_app(app)
# Configure login manager
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info'
@login_manager.user_loader
def load_user(user_id):
from app.models import User
return User.query.get(int(user_id))
# Import models
from app.models import User, Post, PostImage, GPXFile, Comment, Like
# Register blueprints
from app.routes.main import main
app.register_blueprint(main)
from app.routes.auth import auth
app.register_blueprint(auth, url_prefix='/auth')
from app.routes.community import community
app.register_blueprint(community, url_prefix='/community')
# Create upload directories
upload_dir = os.path.join(app.instance_path, 'uploads')
os.makedirs(upload_dir, exist_ok=True)
os.makedirs(os.path.join(upload_dir, 'images'), exist_ok=True)
os.makedirs(os.path.join(upload_dir, 'gpx'), exist_ok=True)
return app
@login_manager.user_loader
def load_user(user_id):
from app.models import User
return User.query.get(int(user_id))

54
app/forms.py Normal file
View File

@@ -0,0 +1,54 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, PasswordField, BooleanField, SelectField, FileField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, NumberRange
from flask_wtf.file import FileAllowed, FileRequired
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()],
render_kw={"placeholder": "Enter your email"})
password = PasswordField('Password', validators=[DataRequired()],
render_kw={"placeholder": "Enter your password"})
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
class RegisterForm(FlaskForm):
nickname = StringField('Nickname', validators=[DataRequired(), Length(min=3, max=20)],
render_kw={"placeholder": "Choose a nickname"})
email = StringField('Email', validators=[DataRequired(), Email()],
render_kw={"placeholder": "Enter your email"})
password = PasswordField('Password', validators=[DataRequired(), Length(min=8)],
render_kw={"placeholder": "Create a password"})
password2 = PasswordField('Confirm Password',
validators=[DataRequired(), EqualTo('password')],
render_kw={"placeholder": "Confirm your password"})
submit = SubmitField('Register')
class ForgotPasswordForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()],
render_kw={"placeholder": "Enter your email"})
submit = SubmitField('Reset Password')
class PostForm(FlaskForm):
title = StringField('Title', validators=[DataRequired(), Length(min=5, max=200)],
render_kw={"placeholder": "Give your adventure a catchy title"})
subtitle = StringField('Subtitle', validators=[Length(max=300)],
render_kw={"placeholder": "A brief description (optional)"})
content = TextAreaField('Content', validators=[DataRequired(), Length(min=50)],
render_kw={"placeholder": "Tell us about your adventure...", "rows": 10})
difficulty = SelectField('Difficulty',
choices=[('1', 'Very Easy'), ('2', 'Easy'), ('3', 'Moderate'),
('4', 'Hard'), ('5', 'Very Hard')],
validators=[DataRequired()], default='3')
images = FileField('Photos',
validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')],
render_kw={"multiple": True, "accept": "image/*"})
gpx_file = FileField('GPX Track',
validators=[FileAllowed(['gpx'], 'GPX files only!')],
render_kw={"accept": ".gpx"})
published = BooleanField('Publish immediately', default=True)
submit = SubmitField('Create Post')
class CommentForm(FlaskForm):
content = TextAreaField('Comment', validators=[DataRequired(), Length(min=10, max=1000)],
render_kw={"placeholder": "Share your thoughts...", "rows": 3})
submit = SubmitField('Post Comment')

126
app/models.py Normal file
View File

@@ -0,0 +1,126 @@
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
db = SQLAlchemy()
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
nickname = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
is_admin = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan')
comments = db.relationship('Comment', backref='author', lazy='dynamic', cascade='all, delete-orphan')
likes = db.relationship('Like', backref='user', lazy='dynamic', cascade='all, delete-orphan')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.nickname}>'
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
subtitle = db.Column(db.String(300))
content = db.Column(db.Text, nullable=False)
difficulty = db.Column(db.Integer, default=3, nullable=False) # 1-5 scale
published = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Foreign Keys
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# Relationships
images = db.relationship('PostImage', backref='post', lazy='dynamic', cascade='all, delete-orphan')
gpx_files = db.relationship('GPXFile', backref='post', lazy='dynamic', cascade='all, delete-orphan')
comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')
likes = db.relationship('Like', backref='post', lazy='dynamic', cascade='all, delete-orphan')
def get_difficulty_label(self):
labels = ['Very Easy', 'Easy', 'Moderate', 'Hard', 'Very Hard']
return labels[self.difficulty - 1] if 1 <= self.difficulty <= 5 else 'Unknown'
def get_like_count(self):
return self.likes.count()
def __repr__(self):
return f'<Post {self.title}>'
class PostImage(db.Model):
__tablename__ = 'post_images'
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
original_name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
size = db.Column(db.Integer, nullable=False)
mime_type = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Foreign Keys
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
def __repr__(self):
return f'<PostImage {self.filename}>'
class GPXFile(db.Model):
__tablename__ = 'gpx_files'
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
original_name = db.Column(db.String(255), nullable=False)
size = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Foreign Keys
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
def __repr__(self):
return f'<GPXFile {self.filename}>'
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Foreign Keys
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
def __repr__(self):
return f'<Comment {self.id}>'
class Like(db.Model):
__tablename__ = 'likes'
id = db.Column(db.Integer, primary_key=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Foreign Keys
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
# Unique constraint
__table_args__ = (db.UniqueConstraint('user_id', 'post_id', name='unique_user_post_like'),)
def __repr__(self):
return f'<Like {self.user_id}-{self.post_id}>'

106
app/routes/auth.py Normal file
View File

@@ -0,0 +1,106 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash
from app.models import User, db
from app.forms import LoginForm, RegisterForm, ForgotPasswordForm
import re
auth = Blueprint('auth', __name__)
@auth.route('/login', methods=['GET', 'POST'])
def login():
"""User login page"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('community.index')
flash(f'Welcome back, {user.nickname}!', 'success')
return redirect(next_page)
else:
flash('Invalid email or password.', 'error')
return render_template('auth/login.html', form=form)
@auth.route('/register', methods=['GET', 'POST'])
def register():
"""User registration page"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegisterForm()
if form.validate_on_submit():
# Check if user already exists
if User.query.filter_by(email=form.email.data).first():
flash('Email address already registered.', 'error')
return render_template('auth/register.html', form=form)
if User.query.filter_by(nickname=form.nickname.data).first():
flash('Nickname already taken.', 'error')
return render_template('auth/register.html', form=form)
# Validate password strength
if not is_valid_password(form.password.data):
flash('Password must be at least 8 characters long and contain at least one letter and one number.', 'error')
return render_template('auth/register.html', form=form)
# Create new user
user = User(
nickname=form.nickname.data,
email=form.email.data
)
user.set_password(form.password.data)
try:
db.session.add(user)
db.session.commit()
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('auth.login'))
except Exception as e:
db.session.rollback()
flash('An error occurred during registration. Please try again.', 'error')
return render_template('auth/register.html', form=form)
@auth.route('/logout')
@login_required
def logout():
"""User logout"""
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('main.index'))
@auth.route('/forgot-password', methods=['GET', 'POST'])
def forgot_password():
"""Forgot password page"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ForgotPasswordForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
# TODO: Implement email sending for password reset
flash('If an account with that email exists, we\'ve sent password reset instructions.', 'info')
else:
flash('If an account with that email exists, we\'ve sent password reset instructions.', 'info')
return redirect(url_for('auth.login'))
return render_template('auth/forgot_password.html', form=form)
def is_valid_password(password):
"""Validate password strength"""
if len(password) < 8:
return False
if not re.search(r'[A-Za-z]', password):
return False
if not re.search(r'\d', password):
return False
return True

195
app/routes/community.py Normal file
View File

@@ -0,0 +1,195 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
from flask_login import login_required, current_user
from app.models import Post, PostImage, GPXFile, User, Comment, Like, db
from app.forms import PostForm, CommentForm
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import os
import uuid
from datetime import datetime
from PIL import Image
import gpxpy
community = Blueprint('community', __name__)
@community.route('/')
def index():
"""Community posts listing page"""
page = request.args.get('page', 1, type=int)
posts = Post.query.filter_by(published=True).order_by(Post.created_at.desc()).paginate(
page=page, per_page=10, error_out=False
)
return render_template('community/index.html', posts=posts)
@community.route('/post/<int:id>')
def post_detail(id):
"""Individual post detail page"""
post = Post.query.get_or_404(id)
if not post.published and (not current_user.is_authenticated or
(current_user.id != post.author_id and not current_user.is_admin)):
flash('Post not found.', 'error')
return redirect(url_for('community.index'))
form = CommentForm()
comments = Comment.query.filter_by(post_id=id).order_by(Comment.created_at.asc()).all()
return render_template('community/post_detail.html', post=post, form=form, comments=comments)
@community.route('/new-post', methods=['GET', 'POST'])
@login_required
def new_post():
"""Create new post page"""
form = PostForm()
if form.validate_on_submit():
try:
# Create post
post = Post(
title=form.title.data,
subtitle=form.subtitle.data,
content=form.content.data,
difficulty=int(form.difficulty.data),
published=form.published.data,
author_id=current_user.id
)
db.session.add(post)
db.session.flush() # Get the post ID
# Handle image uploads
if form.images.data and form.images.data.filename:
images = request.files.getlist('images')
for image_file in images:
if image_file and image_file.filename:
result = save_image(image_file, post.id)
if result['success']:
post_image = PostImage(
filename=result['filename'],
original_name=image_file.filename,
size=result['size'],
mime_type=image_file.content_type,
post_id=post.id
)
db.session.add(post_image)
# Handle GPX file upload
if form.gpx_file.data and form.gpx_file.data.filename:
result = save_gpx_file(form.gpx_file.data, post.id)
if result['success']:
gpx_file = GPXFile(
filename=result['filename'],
original_name=form.gpx_file.data.filename,
size=result['size'],
post_id=post.id
)
db.session.add(gpx_file)
db.session.commit()
flash('Your adventure has been shared!', 'success')
return redirect(url_for('community.post_detail', id=post.id))
except Exception as e:
db.session.rollback()
flash('An error occurred while creating your post. Please try again.', 'error')
current_app.logger.error(f'Error creating post: {str(e)}')
return render_template('community/new_post.html', form=form)
@community.route('/post/<int:id>/comment', methods=['POST'])
@login_required
def add_comment(id):
"""Add comment to post"""
post = Post.query.get_or_404(id)
form = CommentForm()
if form.validate_on_submit():
comment = Comment(
content=form.content.data,
author_id=current_user.id,
post_id=post.id
)
db.session.add(comment)
db.session.commit()
flash('Your comment has been added.', 'success')
return redirect(url_for('community.post_detail', id=id))
@community.route('/post/<int:id>/like', methods=['POST'])
@login_required
def toggle_like(id):
"""Toggle like on post"""
post = Post.query.get_or_404(id)
existing_like = Like.query.filter_by(user_id=current_user.id, post_id=post.id).first()
if existing_like:
db.session.delete(existing_like)
liked = False
else:
like = Like(user_id=current_user.id, post_id=post.id)
db.session.add(like)
liked = True
db.session.commit()
return jsonify({'liked': liked, 'count': post.get_like_count()})
def save_image(image_file, post_id):
"""Save uploaded image file"""
try:
# Create upload directory
upload_dir = os.path.join(current_app.instance_path, 'uploads', 'images')
os.makedirs(upload_dir, exist_ok=True)
# Generate unique filename
filename = secure_filename(f"{uuid.uuid4().hex}_{image_file.filename}")
filepath = os.path.join(upload_dir, filename)
# Save and resize image
image = Image.open(image_file)
if image.mode in ('RGBA', 'LA', 'P'):
image = image.convert('RGB')
# Resize if too large
max_size = (1920, 1080)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
image.save(filepath, 'JPEG', quality=85, optimize=True)
file_size = os.path.getsize(filepath)
return {
'success': True,
'filename': filename,
'size': file_size
}
except Exception as e:
current_app.logger.error(f'Error saving image: {str(e)}')
return {'success': False, 'error': str(e)}
def save_gpx_file(gpx_file, post_id):
"""Save uploaded GPX file"""
try:
# Create upload directory
upload_dir = os.path.join(current_app.instance_path, 'uploads', 'gpx')
os.makedirs(upload_dir, exist_ok=True)
# Generate unique filename
filename = secure_filename(f"{uuid.uuid4().hex}_{gpx_file.filename}")
filepath = os.path.join(upload_dir, filename)
# Validate GPX file
gpx_content = gpx_file.read()
gpx = gpxpy.parse(gpx_content.decode('utf-8'))
# Save file
with open(filepath, 'wb') as f:
f.write(gpx_content)
file_size = len(gpx_content)
return {
'success': True,
'filename': filename,
'size': file_size
}
except Exception as e:
current_app.logger.error(f'Error saving GPX file: {str(e)}')
return {'success': False, 'error': str(e)}

39
app/routes/main.py Normal file
View File

@@ -0,0 +1,39 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from app.models import Post, PostImage, GPXFile, User, db
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import os
import uuid
from datetime import datetime
main = Blueprint('main', __name__)
@main.route('/')
def index():
"""Landing page with about, accommodation, and community sections"""
return render_template('index.html')
@main.route('/favicon.ico')
def favicon():
"""Serve favicon"""
return redirect(url_for('static', filename='favicon.ico'))
@main.route('/health')
def health_check():
"""Health check endpoint"""
return {'status': 'healthy', 'timestamp': datetime.utcnow().isoformat()}
@main.app_errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@main.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500
@main.app_errorhandler(RequestEntityTooLarge)
def file_too_large(error):
flash('File is too large. Maximum file size is 16MB.', 'error')
return redirect(request.url)

BIN
app/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

BIN
app/static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

1
app/templates/README.md Normal file
View File

@@ -0,0 +1 @@
# Create templates directory structure

View File

@@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block title %}Sign In - Moto Adventure{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50 py-20">
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg overflow-hidden">
<div class="bg-gradient-to-r from-blue-500 via-purple-500 to-teal-500 p-6 text-white text-center">
<h2 class="text-2xl font-bold">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Moto Adventure" class="h-8 w-8 inline mr-2">
Welcome Back
</h2>
<p class="text-blue-100 mt-1">Sign in to your account</p>
</div>
<form method="POST" class="p-6 space-y-6">
{{ form.hidden_tag() }}
<div>
{{ form.email.label(class="block text-sm font-medium text-gray-700 mb-1") }}
{{ form.email(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent") }}
{% if form.email.errors %}
<div class="text-red-500 text-sm mt-1">
{% for error in form.email.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
{{ form.password.label(class="block text-sm font-medium text-gray-700 mb-1") }}
{{ form.password(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent") }}
{% if form.password.errors %}
<div class="text-red-500 text-sm mt-1">
{% for error in form.password.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
{{ form.remember_me(class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded") }}
{{ form.remember_me.label(class="ml-2 block text-sm text-gray-700") }}
</div>
<a href="{{ url_for('auth.forgot_password') }}" class="text-sm text-purple-600 hover:text-purple-700">
Forgot password?
</a>
</div>
{{ form.submit(class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-2 px-4 rounded-lg hover:from-blue-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition font-medium") }}
</form>
<div class="px-6 pb-6 text-center">
<p class="text-gray-600">
Don't have an account?
<a href="{{ url_for('auth.register') }}" class="text-blue-600 hover:text-blue-700 font-medium">
Sign up here
</a>
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}Join Community - Moto Adventure{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50 py-20">
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg overflow-hidden">
<div class="bg-gradient-to-r from-emerald-500 via-cyan-500 to-blue-500 p-6 text-white text-center">
<h2 class="text-2xl font-bold">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Moto Adventure" class="h-8 w-8 inline mr-2">
Join the Adventure
</h2>
<p class="text-cyan-100 mt-1">Create your account</p>
</div>
<form method="POST" class="p-6 space-y-6">
{{ form.hidden_tag() }}
<div>
{{ form.nickname.label(class="block text-sm font-medium text-gray-700 mb-1") }}
{{ form.nickname(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent") }}
{% if form.nickname.errors %}
<div class="text-red-500 text-sm mt-1">
{% for error in form.nickname.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
{{ form.email.label(class="block text-sm font-medium text-gray-700 mb-1") }}
{{ form.email(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent") }}
{% if form.email.errors %}
<div class="text-red-500 text-sm mt-1">
{% for error in form.email.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
{{ form.password.label(class="block text-sm font-medium text-gray-700 mb-1") }}
{{ form.password(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent") }}
{% if form.password.errors %}
<div class="text-red-500 text-sm mt-1">
{% for error in form.password.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<p class="text-xs text-gray-500 mt-1">
At least 8 characters with letters and numbers
</p>
</div>
<div>
{{ form.password2.label(class="block text-sm font-medium text-gray-700 mb-1") }}
{{ form.password2(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent") }}
{% if form.password2.errors %}
<div class="text-red-500 text-sm mt-1">
{% for error in form.password2.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
{{ form.submit(class="w-full bg-orange-600 text-white py-2 px-4 rounded-lg hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 transition font-medium") }}
</form>
<div class="px-6 pb-6 text-center">
<p class="text-gray-600">
Already have an account?
<a href="{{ url_for('auth.login') }}" class="text-orange-600 hover:text-orange-700 font-medium">
Sign in here
</a>
</p>
</div>
</div>
</div>
{% endblock %}

173
app/templates/base.html Normal file
View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Moto Adventure Community{% endblock %}</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50">
<!-- Navigation -->
<nav class="bg-gradient-to-r from-blue-600 via-purple-600 to-teal-600 shadow-lg fixed w-full z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<a href="{{ url_for('main.index') }}" class="text-white text-xl font-bold flex items-center">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Moto Adventure" class="h-8 w-8 mr-2">
Moto Adventure
</a>
</div>
<div class="hidden md:flex items-center space-x-8">
<a href="{{ url_for('main.index') }}#about" class="text-white hover:text-blue-200 transition">About</a>
<a href="{{ url_for('main.index') }}#accommodation" class="text-white hover:text-purple-200 transition">Accommodation</a>
<a href="{{ url_for('community.index') }}" class="text-white hover:text-teal-200 transition">Stories & Tracks</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('community.new_post') }}" class="bg-gradient-to-r from-green-500 to-emerald-500 text-white px-4 py-2 rounded-lg hover:from-green-400 hover:to-emerald-400 transition transform hover:scale-105">
<i class="fas fa-plus mr-2"></i>New Post
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.dashboard') }}" class="text-white hover:text-yellow-200 transition">
<i class="fas fa-cog mr-1"></i>Admin
</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}" class="text-white hover:text-red-200 transition">
<i class="fas fa-sign-out-alt mr-1"></i>Logout
</a>
{% else %}
<a href="{{ url_for('auth.login') }}" class="text-white hover:text-blue-200 transition">Login</a>
<a href="{{ url_for('auth.register') }}" class="bg-gradient-to-r from-green-500 to-emerald-500 text-white px-4 py-2 rounded-lg hover:from-green-400 hover:to-emerald-400 transition transform hover:scale-105">Register</a>
{% endif %}
</div>
<!-- Mobile menu button -->
<div class="md:hidden flex items-center">
<button type="button" class="text-white hover:text-orange-200 focus:outline-none focus:text-orange-200" id="mobile-menu-btn">
<i class="fas fa-bars text-xl"></i>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div class="md:hidden bg-gradient-to-r from-blue-700 via-purple-700 to-teal-700 hidden" id="mobile-menu">
<div class="px-2 pt-2 pb-3 space-y-1">
<a href="{{ url_for('main.index') }}#about" class="text-white block px-3 py-2 hover:bg-blue-600 rounded">About</a>
<a href="{{ url_for('main.index') }}#accommodation" class="text-white block px-3 py-2 hover:bg-purple-600 rounded">Accommodation</a>
<a href="{{ url_for('community.index') }}" class="text-white block px-3 py-2 hover:bg-teal-600 rounded">Stories & Tracks</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('community.new_post') }}" class="text-white block px-3 py-2 hover:bg-green-600 rounded">
<i class="fas fa-plus mr-2"></i>New Post
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.dashboard') }}" class="text-white block px-3 py-2 hover:bg-yellow-600 rounded">
<i class="fas fa-cog mr-1"></i>Admin
</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}" class="text-white block px-3 py-2 hover:bg-red-600 rounded">
<i class="fas fa-sign-out-alt mr-1"></i>Logout
</a>
{% else %}
<a href="{{ url_for('auth.login') }}" class="text-white block px-3 py-2 hover:bg-blue-600 rounded">Login</a>
<a href="{{ url_for('auth.register') }}" class="text-white block px-3 py-2 hover:bg-green-600 rounded">Register</a>
{% endif %}
</div>
</div>
</nav>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="fixed top-20 right-4 z-50 space-y-2">
{% for category, message in messages %}
<div class="alert alert-{{ category }} px-4 py-3 rounded-lg shadow-lg max-w-sm
{% if category == 'error' %}bg-red-500 text-white
{% elif category == 'success' %}bg-green-500 text-white
{% elif category == 'warning' %}bg-yellow-500 text-white
{% else %}bg-blue-500 text-white
{% endif %}">
<div class="flex justify-between items-center">
<span>{{ message }}</span>
<button type="button" class="ml-2 text-white hover:text-gray-200" onclick="this.parentElement.parentElement.style.display='none'">
<i class="fas fa-times"></i>
</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Main Content -->
<main class="pt-16">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 class="text-xl font-bold mb-4">
<i class="fas fa-motorcycle mr-2"></i>Moto Adventure
</h3>
<p class="text-gray-300">
Join our community of motorcycle enthusiasts exploring the beautiful landscapes of Romania and beyond.
</p>
</div>
<div>
<h4 class="text-lg font-semibold mb-4">Quick Links</h4>
<ul class="space-y-2">
<li><a href="{{ url_for('main.index') }}#about" class="text-gray-300 hover:text-white transition">About</a></li>
<li><a href="{{ url_for('main.index') }}#accommodation" class="text-gray-300 hover:text-white transition">Accommodation</a></li>
<li><a href="{{ url_for('community.index') }}" class="text-gray-300 hover:text-white transition">Stories & Tracks</a></li>
</ul>
</div>
<div>
<h4 class="text-lg font-semibold mb-4">Contact</h4>
<div class="space-y-2 text-gray-300">
<p><i class="fas fa-map-marker-alt mr-2"></i>Pensiunea Buongusto, Romania</p>
<p><i class="fas fa-envelope mr-2"></i>info@moto-adv.com</p>
<p><i class="fas fa-phone mr-2"></i>+40 123 456 789</p>
</div>
</div>
</div>
<div class="border-t border-gray-700 mt-8 pt-8 text-center text-gray-300">
<p>&copy; 2024 Moto Adventure Community. All rights reserved.</p>
</div>
</div>
</footer>
<script>
// Mobile menu toggle
document.getElementById('mobile-menu-btn').addEventListener('click', function() {
const mobileMenu = document.getElementById('mobile-menu');
mobileMenu.classList.toggle('hidden');
});
// Auto-hide flash messages after 5 seconds
setTimeout(function() {
const alerts = document.querySelectorAll('.alert');
alerts.forEach(function(alert) {
alert.style.display = 'none';
});
}, 5000);
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Page Not Found - Moto Adventure{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50 py-20">
<div class="max-w-2xl mx-auto text-center">
<div class="bg-white rounded-lg shadow-lg p-8">
<div class="mb-6">
<i class="fas fa-exclamation-triangle text-yellow-500 text-6xl mb-4"></i>
<h1 class="text-4xl font-bold text-gray-900 mb-2">404 - Page Not Found</h1>
<p class="text-xl text-gray-600">
Oops! The page you're looking for seems to have taken a detour.
</p>
</div>
<div class="bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-6 mb-6">
<p class="text-gray-700 mb-4">
Don't worry, every great adventure has its unexpected turns!
Let's get you back on the right path.
</p>
</div>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('main.index') }}" class="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-6 py-3 rounded-lg font-semibold hover:from-blue-700 hover:to-purple-700 transition transform hover:scale-105">
<i class="fas fa-home mr-2"></i>Back to Home
</a>
<a href="{{ url_for('community.index') }}" class="border-2 border-blue-600 text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-blue-600 hover:text-white transition transform hover:scale-105">
<i class="fas fa-users mr-2"></i>Browse Community
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Server Error - Moto Adventure{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50 py-20">
<div class="max-w-2xl mx-auto text-center">
<div class="bg-white rounded-lg shadow-lg p-8">
<div class="mb-6">
<i class="fas fa-tools text-red-500 text-6xl mb-4"></i>
<h1 class="text-4xl font-bold text-gray-900 mb-2">500 - Server Error</h1>
<p class="text-xl text-gray-600">
Something went wrong on our end. We're working to fix it!
</p>
</div>
<div class="bg-gradient-to-r from-red-50 to-orange-50 rounded-lg p-6 mb-6">
<p class="text-gray-700 mb-4">
Our technical team has been notified and is working to resolve the issue.
Please try again in a few moments.
</p>
</div>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('main.index') }}" class="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-6 py-3 rounded-lg font-semibold hover:from-blue-700 hover:to-purple-700 transition transform hover:scale-105">
<i class="fas fa-home mr-2"></i>Back to Home
</a>
<button onclick="window.location.reload()" class="border-2 border-blue-600 text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-blue-600 hover:text-white transition transform hover:scale-105">
<i class="fas fa-redo mr-2"></i>Try Again
</button>
</div>
</div>
</div>
</div>
{% endblock %}

275
app/templates/index.html Normal file
View File

@@ -0,0 +1,275 @@
{% extends "base.html" %}
{% block title %}Moto Adventure Community - Explore Romania on Two Wheels{% endblock %}
{% block content %}
<!-- Hero Section -->
<section class="relative bg-gradient-to-br from-blue-600 via-purple-600 to-teal-600 text-white py-24" style="background-image: url('{{ url_for('static', filename='images/pano transalpina.jpg') }}'); background-size: cover; background-position: center; background-blend-mode: overlay;">
<div class="absolute inset-0 bg-black opacity-40"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="flex justify-center mb-6">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Moto Adventure" class="h-24 w-24 mb-4">
</div>
<h1 class="text-5xl md:text-7xl font-bold mb-6 leading-tight drop-shadow-lg">
Adventure Awaits
</h1>
<p class="text-xl md:text-2xl mb-8 max-w-3xl mx-auto leading-relaxed drop-shadow-md">
Discover Romania's most breathtaking motorcycle routes, connect with fellow riders,
and find the perfect accommodation for your next adventure.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="#about" class="bg-white text-blue-600 px-8 py-4 rounded-lg font-semibold text-lg hover:bg-gray-100 transition transform hover:scale-105 shadow-lg">
<i class="fas fa-compass mr-2"></i>Start Exploring
</a>
<a href="{{ url_for('community.index') }}" class="border-2 border-white text-white px-8 py-4 rounded-lg font-semibold text-lg hover:bg-white hover:text-purple-600 transition transform hover:scale-105">
<i class="fas fa-users mr-2"></i>Join Community
</a>
</div>
</div>
</section>
<!-- About Section -->
<section id="about" class="py-20 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-4xl font-bold text-gray-900 mb-4">
<i class="fas fa-route text-blue-600 mr-3"></i>
Discover Romania's Hidden Gems
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Join our passionate community of motorcycle enthusiasts as we explore the most spectacular
routes Romania has to offer. From winding mountain passes to scenic coastal roads.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div class="space-y-6">
<div class="flex items-start space-x-4">
<div class="bg-blue-100 p-3 rounded-lg">
<i class="fas fa-mountain text-blue-600 text-xl"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Epic Mountain Routes</h3>
<p class="text-gray-600">
Experience breathtaking rides through the Carpathian Mountains, including the famous
Transfăgărășan and Transalpina highways.
</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="bg-purple-100 p-3 rounded-lg">
<i class="fas fa-users text-purple-600 text-xl"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Vibrant Community</h3>
<p class="text-gray-600">
Connect with fellow riders, share your adventures, and discover new routes through
our active community platform.
</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="bg-teal-100 p-3 rounded-lg">
<i class="fas fa-map-marked-alt text-teal-600 text-xl"></i>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">GPS Tracks & Guides</h3>
<p class="text-gray-600">
Download detailed GPS tracks, get insider tips, and access comprehensive guides
for the best motorcycle routes.
</p>
</div>
</div>
</div>
<div class="relative">
<div class="bg-gradient-to-br from-emerald-400 via-cyan-400 to-blue-500 rounded-2xl p-8 text-white shadow-2xl transform rotate-3 hover:rotate-0 transition-transform duration-300">
<div class="bg-white bg-opacity-20 rounded-lg p-6 mb-6">
<i class="fas fa-motorcycle text-4xl mb-4"></i>
<h3 class="text-2xl font-bold mb-2">Adventure Stats</h3>
</div>
<div class="grid grid-cols-2 gap-6">
<div class="text-center">
<div class="text-3xl font-bold">150+</div>
<div class="text-blue-100">Routes Mapped</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold">500+</div>
<div class="text-cyan-100">Active Riders</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold">50K+</div>
<div class="text-emerald-100">KMs Explored</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold">25+</div>
<div class="text-blue-100">Counties Covered</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Accommodation Section -->
<section id="accommodation" class="py-20 bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-4xl font-bold text-gray-900 mb-4">
<i class="fas fa-bed text-purple-600 mr-3"></i>
Pensiune BuonGusto
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Your perfect base camp for motorcycle adventures. Enjoy comfortable accommodation,
secure parking, and warm hospitality in the heart of Romania's scenic landscapes.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div class="space-y-6">
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-blue-500">
<h3 class="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<i class="fas fa-shield-alt text-blue-600 mr-2"></i>
Secure Motorcycle Parking
</h3>
<p class="text-gray-600">
Rest easy knowing your motorcycle is safe in our covered, monitored parking area.
We understand how precious your bike is to you.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-teal-500">
<h3 class="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<i class="fas fa-map-marked-alt text-teal-600 mr-2"></i>
Local Route Expertise
</h3>
<p class="text-gray-600">
Our staff are passionate about motorcycling and can recommend the best local routes,
hidden gems, and must-see attractions in the area.
</p>
</div>
</div>
<div class="relative">
<div class="bg-white rounded-2xl shadow-2xl overflow-hidden">
<div class="bg-gradient-to-r from-purple-500 to-blue-500 p-6 text-white">
<h3 class="text-2xl font-bold mb-2">
<i class="fas fa-star mr-2"></i>
Premium Accommodation
</h3>
<p class="text-blue-100">Experience comfort and convenience</p>
</div>
<div class="p-6 space-y-4">
<div class="flex items-center justify-between">
<span class="text-gray-600">Comfortable Rooms</span>
<i class="fas fa-check text-green-500"></i>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">Free WiFi</span>
<i class="fas fa-check text-green-500"></i>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">Draft Beers for Responsible Drivers</span>
<i class="fas fa-check text-green-500"></i>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">Route Planning</span>
<i class="fas fa-check text-green-500"></i>
</div>
<div class="border-t pt-4">
<div class="flex items-center justify-between text-lg font-semibold">
<span>Starting from</span>
<span class="text-purple-600">€45/night</span>
</div>
</div>
<a href="https://buongusto.ro/index.php/book-now/" target="_blank" class="w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-blue-700 transition transform hover:scale-105 block text-center">
<i class="fas fa-calendar-alt mr-2"></i>
Book Now
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Community Section -->
<section id="community" class="py-20 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-4xl font-bold text-gray-900 mb-4">
<i class="fas fa-users text-teal-600 mr-3"></i>
Stories & Tracks Community
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Share your adventures, discover new routes, and connect with fellow motorcycle enthusiasts.
Every ride has a story - what's yours?
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
<div class="text-center group">
<div class="bg-emerald-100 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-emerald-200 transition">
<i class="fas fa-camera text-emerald-600 text-2xl"></i>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Share Photos</h3>
<p class="text-gray-600">
Upload stunning photos from your rides and inspire others to explore new destinations.
</p>
</div>
<div class="text-center group">
<div class="bg-blue-100 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-blue-200 transition">
<i class="fas fa-route text-blue-600 text-2xl"></i>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Upload GPS Tracks</h3>
<p class="text-gray-600">
Share your favorite routes with detailed GPS tracks that others can download and follow.
</p>
</div>
<div class="text-center group">
<div class="bg-purple-100 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-purple-200 transition">
<i class="fas fa-comments text-purple-600 text-2xl"></i>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Connect & Discuss</h3>
<p class="text-gray-600">
Join conversations, get tips, and plan group rides with our active community.
</p>
</div>
</div>
<div class="text-center">
<div class="bg-gradient-to-r from-emerald-500 via-cyan-500 to-blue-500 rounded-2xl p-8 text-white">
<h3 class="text-2xl font-bold mb-4">Ready to Share Your Adventure?</h3>
<p class="text-blue-100 mb-6 text-lg">
Join thousands of riders who are already sharing their stories and discovering new routes.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
{% if current_user.is_authenticated %}
<a href="{{ url_for('community.new_post') }}" class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition transform hover:scale-105">
<i class="fas fa-plus mr-2"></i>Create New Post
</a>
<a href="{{ url_for('community.index') }}" class="border-2 border-white text-white px-8 py-3 rounded-lg font-semibold hover:bg-white hover:text-cyan-600 transition transform hover:scale-105">
<i class="fas fa-eye mr-2"></i>Browse Stories
</a>
{% else %}
<a href="{{ url_for('auth.register') }}" class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition transform hover:scale-105">
<i class="fas fa-user-plus mr-2"></i>Join Community
</a>
<a href="{{ url_for('community.index') }}" class="border-2 border-white text-white px-8 py-3 rounded-lg font-semibold hover:bg-white hover:text-cyan-600 transition transform hover:scale-105">
<i class="fas fa-eye mr-2"></i>Browse Stories
</a>
{% endif %}
</div>
</div>
</div>
</div>
</section>
{% endblock %}

48
config.py Normal file
View File

@@ -0,0 +1,48 @@
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://postgres:password@localhost/moto_adventure'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# File Upload Configuration
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'app/static/uploads'
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'gpx'}
# Email Configuration
MAIL_SERVER = os.environ.get('MAIL_SERVER') or 'smtp.gmail.com'
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER') or 'noreply@moto-adv.com'
# Pensiunea Buongusto Configuration
PENSIUNEA_PHONE = os.environ.get('PENSIUNEA_PHONE') or '+40-xxx-xxx-xxx'
PENSIUNEA_EMAIL = os.environ.get('PENSIUNEA_EMAIL') or 'info@pensiunebuongusto.ro'
PENSIUNEA_WEBSITE = os.environ.get('PENSIUNEA_WEBSITE') or 'https://pensiunebuongusto.ro'
# Redis Configuration (for Celery)
REDIS_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379/0'
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'postgresql://postgres:password@localhost/moto_adventure_dev'
class ProductionConfig(Config):
DEBUG = False
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}

46
docker-compose.yml Normal file
View File

@@ -0,0 +1,46 @@
version: '3.8'
services:
app:
build: .
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://moto_user:moto_pass@db:5432/moto_adventure
- SECRET_KEY=your-super-secret-key-change-this
- MAIL_SERVER=smtp.gmail.com
- MAIL_PORT=587
- MAIL_USE_TLS=true
- MAIL_USERNAME=your-email@gmail.com
- MAIL_PASSWORD=your-app-password
volumes:
- ./uploads:/opt/site/flask-moto-adventure/uploads
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15
environment:
- POSTGRES_DB=moto_adventure
- POSTGRES_USER=moto_user
- POSTGRES_PASSWORD=moto_pass
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./uploads:/opt/site/flask-moto-adventure/uploads:ro
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:

5
next-env.d.ts vendored
View File

@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

@@ -1,15 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['localhost', 'res.cloudinary.com', 'images.unsplash.com'],
},
webpack: (config) => {
config.module.rules.push({
test: /\.gpx$/,
use: 'raw-loader',
});
return config;
},
};
module.exports = nextConfig;

8179
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +0,0 @@
{
"name": "moto-adv-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.18",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@types/node": "^20.17.9",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"typescript": "^5.7.2",
"tailwindcss": "^3.4.17",
"postcss": "^8.4.41",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.18",
"framer-motion": "^11.11.17",
"lucide-react": "^0.469.0",
"react-hook-form": "^7.54.0",
"next-auth": "^4.24.10",
"@next-auth/prisma-adapter": "^1.0.7",
"prisma": "^5.22.0",
"@prisma/client": "^5.22.0",
"bcryptjs": "^2.4.3",
"@types/bcryptjs": "^2.4.6",
"react-leaflet": "^4.2.1",
"leaflet": "^1.9.4",
"@types/leaflet": "^1.9.14",
"gpxparser": "^3.0.8",
"multer": "^1.4.5-lts.1",
"@types/multer": "^1.4.12",
"sharp": "^0.33.5",
"react-dropzone": "^14.2.10",
"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",
"@tailwindcss/forms": "^0.5.9"
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,144 +0,0 @@
// 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")
}

19
requirements.txt Normal file
View File

@@ -0,0 +1,19 @@
Flask==3.0.0
Werkzeug==3.0.1
Jinja2==3.1.2
SQLAlchemy==2.0.23
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.5
Flask-Login==0.6.3
Flask-WTF==1.2.1
WTForms==3.1.1
Flask-Mail==0.9.1
Pillow==10.1.0
python-dotenv==1.0.0
bcrypt==4.1.2
email-validator==2.1.0
gpxpy==1.5.0
gunicorn==21.2.0
psycopg2-binary==2.9.9
redis==5.0.1
celery==5.3.4

BIN
resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

48
run.py Normal file
View File

@@ -0,0 +1,48 @@
import os
from app import create_app, db
from app.models import User, Post, PostImage, GPXFile, Comment, Like
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {
'db': db,
'User': User,
'Post': Post,
'PostImage': PostImage,
'GPXFile': GPXFile,
'Comment': Comment,
'Like': Like
}
@app.cli.command()
def init_db():
"""Initialize the database."""
db.create_all()
print('Database initialized.')
@app.cli.command()
def create_admin():
"""Create an admin user."""
admin_email = os.environ.get('ADMIN_EMAIL', 'admin@moto-adv.com')
admin_password = os.environ.get('ADMIN_PASSWORD', 'admin123')
admin = User.query.filter_by(email=admin_email).first()
if admin:
print(f'Admin user {admin_email} already exists.')
return
admin = User(
nickname='admin',
email=admin_email,
is_admin=True
)
admin.set_password(admin_password)
db.session.add(admin)
db.session.commit()
print(f'Admin user created: {admin_email}')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)

View File

@@ -1,428 +0,0 @@
'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

@@ -1,238 +0,0 @@
'use client'
import { motion } from 'framer-motion'
import { MapPin, Clock, Camera, Heart, Share2, Calendar } from 'lucide-react'
import Header from '@/components/Header'
import Image from 'next/image'
export default function AdventuresPage() {
const adventures = [
{
id: 1,
title: "Pacific Coast Highway Adventure",
location: "California, USA",
duration: "7 days",
distance: "650 miles",
difficulty: "Intermediate",
likes: 234,
date: "2024-06-15",
image: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80",
description: "An epic coastal ride through California's most scenic highways, featuring breathtaking ocean views and winding mountain roads.",
tags: ["coastal", "scenic", "mountains"]
},
{
id: 2,
title: "Alpine Loop Challenge",
location: "Swiss Alps",
duration: "5 days",
distance: "800 miles",
difficulty: "Advanced",
likes: 189,
date: "2024-07-20",
image: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80",
description: "Navigate through stunning Alpine passes with challenging curves and spectacular mountain vistas.",
tags: ["mountains", "challenging", "scenic"]
},
{
id: 3,
title: "Desert Sunset Expedition",
location: "Arizona, USA",
duration: "3 days",
distance: "450 miles",
difficulty: "Beginner",
likes: 156,
date: "2024-05-10",
image: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80",
description: "Experience the magic of desert landscapes with incredible sunset views and star-filled nights.",
tags: ["desert", "sunset", "easy"]
},
{
id: 4,
title: "Northern Lights Quest",
location: "Norway",
duration: "10 days",
distance: "1200 miles",
difficulty: "Advanced",
likes: 312,
date: "2024-03-01",
image: "https://images.unsplash.com/photo-1531366936337-7c912a4589a7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80",
description: "Chase the Aurora Borealis through Norway's dramatic fjords and Arctic landscapes.",
tags: ["arctic", "northern lights", "fjords"]
},
{
id: 5,
title: "Tuscany Wine Trail",
location: "Tuscany, Italy",
duration: "6 days",
distance: "520 miles",
difficulty: "Intermediate",
likes: 278,
date: "2024-08-05",
image: "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80",
description: "Ride through rolling hills, vineyards, and medieval towns in Italy's most beautiful region.",
tags: ["vineyards", "culture", "hills"]
},
{
id: 6,
title: "Great Ocean Road",
location: "Victoria, Australia",
duration: "4 days",
distance: "380 miles",
difficulty: "Beginner",
likes: 201,
date: "2024-04-18",
image: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1000&q=80",
description: "Explore Australia's iconic coastal route with limestone cliffs, beaches, and rainforest sections.",
tags: ["coastal", "iconic", "beaches"]
}
]
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'Beginner': return 'bg-green-100 text-green-800'
case 'Intermediate': return 'bg-yellow-100 text-yellow-800'
case 'Advanced': return 'bg-red-100 text-red-800'
default: return 'bg-gray-100 text-gray-800'
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
<Header />
{/* Hero Section */}
<section className="pt-16 bg-gradient-to-r from-blue-600 to-purple-600 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center"
>
<h1 className="text-4xl md:text-5xl font-bold mb-6">
Epic Adventures Await
</h1>
<p className="text-xl md:text-2xl text-blue-100 mb-8 max-w-3xl mx-auto">
Discover incredible motorcycle journeys from riders around the world.
Get inspired, plan your next trip, and share your own adventures.
</p>
<div className="flex flex-wrap justify-center gap-4">
<button className="bg-white text-blue-600 px-6 py-3 rounded-lg font-medium hover:bg-blue-50 transition-colors">
Share Your Adventure
</button>
<button className="border border-white text-white px-6 py-3 rounded-lg font-medium hover:bg-white hover:text-blue-600 transition-colors">
Browse by Region
</button>
</div>
</motion.div>
</div>
</section>
{/* Filters */}
<section className="py-8 bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-wrap gap-4 items-center justify-between">
<div className="flex flex-wrap gap-2">
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium">All</button>
<button className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">Coastal</button>
<button className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">Mountains</button>
<button className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">Desert</button>
<button className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">Urban</button>
</div>
<select className="px-4 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option>Sort by Date</option>
<option>Sort by Popularity</option>
<option>Sort by Distance</option>
<option>Sort by Difficulty</option>
</select>
</div>
</div>
</section>
{/* Adventures Grid */}
<section className="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-2 lg:grid-cols-3 gap-8">
{adventures.map((adventure, index) => (
<motion.div
key={adventure.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-white rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 group cursor-pointer"
>
<div className="relative h-64 overflow-hidden">
<div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500" />
<div className="absolute inset-0 bg-black/20" />
<div className="absolute top-4 left-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getDifficultyColor(adventure.difficulty)}`}>
{adventure.difficulty}
</span>
</div>
<div className="absolute top-4 right-4 flex gap-2">
<button className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors">
<Heart className="w-4 h-4" />
</button>
<button className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors">
<Share2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="p-6">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-2">
<Calendar className="w-4 h-4" />
{new Date(adventure.date).toLocaleDateString()}
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors">
{adventure.title}
</h3>
<div className="flex items-center gap-2 text-gray-600 mb-3">
<MapPin className="w-4 h-4" />
<span className="text-sm">{adventure.location}</span>
</div>
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
{adventure.description}
</p>
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
{adventure.duration}
</div>
<div>{adventure.distance}</div>
</div>
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-1">
{adventure.tags.slice(0, 2).map((tag) => (
<span key={tag} className="px-2 py-1 bg-blue-50 text-blue-600 text-xs rounded-full">
{tag}
</span>
))}
</div>
<div className="flex items-center gap-1 text-gray-500">
<Heart className="w-4 h-4" />
<span className="text-sm">{adventure.likes}</span>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Load More */}
<section className="py-8 text-center">
<button className="bg-blue-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors">
Load More Adventures
</button>
</section>
</div>
)
}

View File

@@ -1,468 +0,0 @@
'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>
)
}

View File

@@ -1,269 +0,0 @@
'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,93 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles for motorcycle adventure theme */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 98%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* Adventure theme colors */
.adventure-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.hero-bg {
background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6)),
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><pattern id="a" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="2" fill="%23ffffff" opacity="0.1"/></pattern></defs><rect width="100%" height="100%" fill="%23111827"/><rect width="100%" height="100%" fill="url(%23a)"/></svg>');
}
/* Smooth scroll */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@@ -1,35 +0,0 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Moto Adventure - Epic Motorcycle Journeys',
description: 'Discover epic motorcycle adventures, share your journeys, and connect with fellow riders. Track your routes, share stories, and explore the world on two wheels.',
keywords: 'motorcycle, adventure, riding, travel, GPS, routes, touring, bike',
authors: [{ name: 'Moto Adventure Team' }],
openGraph: {
title: 'Moto Adventure - Epic Motorcycle Journeys',
description: 'Discover epic motorcycle adventures and share your journeys with fellow riders.',
url: 'https://moto-adv.com',
siteName: 'Moto Adventure',
type: 'website',
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="scroll-smooth">
<body className={inter.className}>
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
{children}
</div>
</body>
</html>
)
}

View File

@@ -1,398 +0,0 @@
'use client'
import { motion } from 'framer-motion'
import { MapPin, Users, Home, ArrowRight, Star, Bed, Phone, Mail, Download } from 'lucide-react'
import Link from 'next/link'
export default function HomePage() {
return (
<div className="min-h-screen">
{/* Navigation */}
<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-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="#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>
<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 - 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 lg:text-7xl font-bold text-white mb-6"
>
Welcome to Your
<span className="block bg-gradient-to-r from-yellow-400 to-orange-500 bg-clip-text text-transparent">
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-200 mb-12 max-w-4xl mx-auto leading-relaxed"
>
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 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
>
<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" />
</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>
{/* 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">
<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">
Created for Our Community
</h2>
<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-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-3 gap-8">
<div>
<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">Quick Links</h4>
<ul className="space-y-2 text-gray-400">
<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">Contact</h4>
<ul className="space-y-2 text-gray-400">
<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 Community. Built with for riders.</p>
</div>
</div>
</footer>
</div>
)
}

View File

@@ -1,280 +0,0 @@
'use client'
import { motion } from 'framer-motion'
import { MapPin, Download, Star, Filter, Search, Upload } from 'lucide-react'
import Header from '@/components/Header'
export default function RoutesPage() {
const routes = [
{
id: 1,
name: "Coastal Highway 1",
distance: "145 miles",
difficulty: "Intermediate",
rating: 4.8,
downloads: 2341,
startPoint: "San Francisco, CA",
endPoint: "Monterey, CA",
terrain: "Coastal",
description: "Stunning coastal views with challenging curves and breathtaking cliff-side roads.",
tags: ["scenic", "coastal", "curves"]
},
{
id: 2,
name: "Alpine Adventure Loop",
distance: "230 miles",
difficulty: "Advanced",
rating: 4.9,
downloads: 1876,
startPoint: "Interlaken, Switzerland",
endPoint: "Interlaken, Switzerland",
terrain: "Mountain",
description: "High-altitude mountain passes with snow-capped peaks and pristine lakes.",
tags: ["mountains", "challenging", "scenic"]
},
{
id: 3,
name: "Desert Sunrise Route",
distance: "89 miles",
difficulty: "Beginner",
rating: 4.6,
downloads: 3102,
startPoint: "Phoenix, AZ",
endPoint: "Sedona, AZ",
terrain: "Desert",
description: "Easy desert ride perfect for beginners with amazing sunrise views.",
tags: ["desert", "easy", "sunrise"]
},
{
id: 4,
name: "Black Forest Trail",
distance: "167 miles",
difficulty: "Intermediate",
rating: 4.7,
downloads: 1654,
startPoint: "Stuttgart, Germany",
endPoint: "Freiburg, Germany",
terrain: "Forest",
description: "Winding forest roads through Germany's famous Black Forest region.",
tags: ["forest", "winding", "cultural"]
},
{
id: 5,
name: "Fjord Explorer",
distance: "312 miles",
difficulty: "Advanced",
rating: 4.9,
downloads: 987,
startPoint: "Bergen, Norway",
endPoint: "Geiranger, Norway",
terrain: "Fjord",
description: "Epic fjord landscapes with waterfalls and dramatic mountain views.",
tags: ["fjords", "waterfalls", "dramatic"]
},
{
id: 6,
name: "Tuscan Hills Circuit",
distance: "124 miles",
difficulty: "Intermediate",
rating: 4.8,
downloads: 2198,
startPoint: "Florence, Italy",
endPoint: "Siena, Italy",
terrain: "Hills",
description: "Rolling hills through vineyards and medieval towns in Tuscany.",
tags: ["hills", "vineyards", "historic"]
}
]
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'Beginner': return 'bg-green-100 text-green-800 border-green-200'
case 'Intermediate': return 'bg-yellow-100 text-yellow-800 border-yellow-200'
case 'Advanced': return 'bg-red-100 text-red-800 border-red-200'
default: return 'bg-gray-100 text-gray-800 border-gray-200'
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
<Header />
{/* Hero Section */}
<section className="pt-16 bg-gradient-to-r from-green-600 to-blue-600 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center"
>
<h1 className="text-4xl md:text-5xl font-bold mb-6">
Discover Epic Routes
</h1>
<p className="text-xl md:text-2xl text-green-100 mb-8 max-w-3xl mx-auto">
Download GPS tracks from fellow riders worldwide. Find your perfect route
based on difficulty, terrain, and scenic beauty.
</p>
<div className="flex flex-wrap justify-center gap-4">
<button className="bg-white text-green-600 px-6 py-3 rounded-lg font-medium hover:bg-green-50 transition-colors flex items-center gap-2">
<Upload className="w-5 h-5" />
Upload Your Route
</button>
<button className="border border-white text-white px-6 py-3 rounded-lg font-medium hover:bg-white hover:text-green-600 transition-colors">
Browse by Country
</button>
</div>
</motion.div>
</div>
</section>
{/* Search and Filters */}
<section className="py-8 bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col lg:flex-row gap-4 items-center justify-between">
{/* Search */}
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
placeholder="Search routes by name or location..."
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 items-center">
<select className="px-4 py-3 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500">
<option>All Difficulties</option>
<option>Beginner</option>
<option>Intermediate</option>
<option>Advanced</option>
</select>
<select className="px-4 py-3 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500">
<option>All Terrains</option>
<option>Coastal</option>
<option>Mountain</option>
<option>Desert</option>
<option>Forest</option>
<option>Urban</option>
</select>
<select className="px-4 py-3 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500">
<option>Sort by Popularity</option>
<option>Sort by Distance</option>
<option>Sort by Rating</option>
<option>Sort by Date Added</option>
</select>
</div>
</div>
</div>
</section>
{/* Routes Grid */}
<section className="py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{routes.map((route, index) => (
<motion.div
key={route.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-white rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 group"
>
<div className="relative h-48 bg-gradient-to-br from-green-400 to-blue-500">
<div className="absolute inset-0 bg-black/20" />
<div className="absolute top-4 left-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${getDifficultyColor(route.difficulty)}`}>
{route.difficulty}
</span>
</div>
<div className="absolute top-4 right-4">
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm rounded-full px-2 py-1 text-white text-sm">
<Star className="w-4 h-4 fill-current" />
{route.rating}
</div>
</div>
<div className="absolute bottom-4 left-4 right-4">
<h3 className="text-xl font-bold text-white mb-1 group-hover:text-yellow-300 transition-colors">
{route.name}
</h3>
<div className="flex items-center gap-2 text-white/90 text-sm">
<MapPin className="w-4 h-4" />
<span>{route.startPoint} {route.endPoint}</span>
</div>
</div>
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="text-2xl font-bold text-gray-900">{route.distance}</div>
<div className="text-right">
<div className="text-sm text-gray-500">Terrain</div>
<div className="font-semibold text-gray-900">{route.terrain}</div>
</div>
</div>
<p className="text-gray-600 text-sm mb-4">
{route.description}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{route.tags.map((tag) => (
<span key={tag} className="px-2 py-1 bg-green-50 text-green-600 text-xs rounded-full">
{tag}
</span>
))}
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<div className="flex items-center gap-2 text-gray-500">
<Download className="w-4 h-4" />
<span className="text-sm">{route.downloads.toLocaleString()} downloads</span>
</div>
<button className="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors flex items-center gap-2">
<Download className="w-4 h-4" />
Download GPX
</button>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Upload Section */}
<section className="py-16 bg-gradient-to-r from-green-600 to-blue-600">
<div className="max-w-4xl 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 }}
>
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
Share Your Favorite Routes
</h2>
<p className="text-xl text-green-100 mb-8 max-w-2xl mx-auto">
Help fellow riders discover amazing routes by sharing your GPX tracks and experiences.
</p>
<button className="bg-white text-green-600 px-8 py-4 rounded-lg text-lg font-bold hover:bg-green-50 transition-colors flex items-center gap-2 mx-auto">
<Upload className="w-5 h-5" />
Upload Your Route
</button>
</motion.div>
</div>
</section>
{/* Load More */}
<section className="py-8 text-center">
<button className="bg-green-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-green-700 transition-colors">
Load More Routes
</button>
</section>
</div>
)
}

View File

@@ -1,36 +0,0 @@
'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

@@ -1,80 +0,0 @@
'use client'
import { useState } from 'react'
import { Menu, X } from 'lucide-react'
import Link from 'next/link'
export default function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const navigation = [
{ name: 'Home', href: '/' },
{ name: 'Adventures', href: '/adventures' },
{ name: 'Routes', href: '/routes' },
{ name: 'Community', href: '/community' },
{ name: 'Blog', href: '/blog' },
]
return (
<nav className="fixed top-0 w-full bg-white/90 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">
<Link href="/" className="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>
</Link>
</div>
{/* Desktop Navigation */}
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className="text-gray-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
{item.name}
</Link>
))}
<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>
</div>
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="text-gray-600 hover:text-gray-900 p-2"
>
{isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
{/* Mobile Navigation */}
{isMenuOpen && (
<div className="md:hidden">
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-white border-t border-gray-200">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className="text-gray-600 hover:text-blue-600 block px-3 py-2 rounded-md text-base font-medium transition-colors"
onClick={() => setIsMenuOpen(false)}
>
{item.name}
</Link>
))}
<button className="w-full text-left bg-blue-600 text-white px-3 py-2 rounded-lg text-base font-medium hover:bg-blue-700 transition-colors mt-4">
Get Started
</button>
</div>
</div>
)}
</div>
</nav>
)
}

View File

@@ -1,69 +0,0 @@
'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>
)
}

View File

@@ -1,73 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: {
50: '#fef7ee',
100: '#fcecd6',
200: '#f8d4ad',
300: '#f3b679',
400: '#ec8f43',
500: '#e8711e',
600: '#d95914',
700: '#b44213',
800: '#903517',
900: '#752d15',
950: '#3f1408',
},
dark: {
50: '#f6f6f6',
100: '#e7e7e7',
200: '#d1d1d1',
300: '#b0b0b0',
400: '#888888',
500: '#6d6d6d',
600: '#5d5d5d',
700: '#4f4f4f',
800: '#454545',
900: '#3d3d3d',
950: '#1a1a1a',
},
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
'hero-pattern': "url('/images/hero-bg.jpg')",
},
fontFamily: {
'adventure': ['Roboto Condensed', 'sans-serif'],
'body': ['Inter', 'sans-serif'],
},
animation: {
'parallax': 'parallax 1s ease-out forwards',
'fade-in': 'fadeIn 0.6s ease-out forwards',
'slide-up': 'slideUp 0.8s ease-out forwards',
},
keyframes: {
parallax: {
'0%': { transform: 'translateY(0px)' },
'100%': { transform: 'translateY(-50px)' },
},
fadeIn: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0px)' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(60px)' },
'100%': { opacity: '1', transform: 'translateY(0px)' },
},
},
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
}

View File

@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}