init
This commit is contained in:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# uploaded files
|
||||||
|
/public/uploads
|
||||||
|
|
||||||
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 420Deals.ch
|
||||||
|
|
||||||
|
A premium collective buying platform for CBD in Switzerland.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
1. Create the database using the provided SQL file:
|
||||||
|
```bash
|
||||||
|
mysql -u root -p < cbd420.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the migration to add image support (optional but recommended):
|
||||||
|
```bash
|
||||||
|
mysql -u root -p cbd420 < migrations/add_image_url.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a `.env.local` file in the root directory:
|
||||||
|
```env
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
DB_NAME=cbd420
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit [http://localhost:3000](http://localhost:3000) for the main site and [http://localhost:3000/admin](http://localhost:3000/admin) for the admin panel.
|
||||||
|
|
||||||
|
## Admin Panel
|
||||||
|
|
||||||
|
Access the admin panel at `/admin` to:
|
||||||
|
- Create new drops
|
||||||
|
- View all drops
|
||||||
|
- Monitor drop progress and sold out status
|
||||||
|
|
||||||
|
### Creating a Drop
|
||||||
|
|
||||||
|
1. Navigate to `/admin`
|
||||||
|
2. Fill in the form:
|
||||||
|
- **Product Name**: e.g., "Harlequin – Collective Drop"
|
||||||
|
- **Batch Size**: Total amount (e.g., 1000)
|
||||||
|
- **Unit**: Custom unit (e.g., g, kg, ml, etc.)
|
||||||
|
- **Price Per Gram**: Price in CHF (e.g., 2.50)
|
||||||
|
- **Product Image**: Optional product image upload (JPEG, PNG, WebP, max 5MB)
|
||||||
|
3. Click "Create Drop"
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `app/` - Next.js app directory
|
||||||
|
- `api/drops/` - API routes for drop management
|
||||||
|
- `admin/` - Admin panel page
|
||||||
|
- `components/` - React components
|
||||||
|
- `lib/db.ts` - Database connection pool
|
||||||
|
- `cbd420.sql` - Database schema
|
||||||
324
app/admin/page.tsx
Normal file
324
app/admin/page.tsx
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
interface Drop {
|
||||||
|
id: number
|
||||||
|
item: string
|
||||||
|
size: number
|
||||||
|
fill: number
|
||||||
|
unit: string
|
||||||
|
ppu: number
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [drops, setDrops] = useState<Drop[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [uploadingImage, setUploadingImage] = useState(false)
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
item: '',
|
||||||
|
size: '',
|
||||||
|
unit: 'g',
|
||||||
|
ppu: '',
|
||||||
|
imageFile: null as File | null,
|
||||||
|
imagePreview: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDrops()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchDrops = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/drops')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch drops')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
// Ensure data is always an array
|
||||||
|
setDrops(Array.isArray(data) ? data : [])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching drops:', error)
|
||||||
|
setDrops([]) // Set to empty array on error
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
imageFile: file,
|
||||||
|
imagePreview: URL.createObjectURL(file),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setSubmitting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
let imageUrl = ''
|
||||||
|
|
||||||
|
// Upload image if provided
|
||||||
|
if (formData.imageFile) {
|
||||||
|
setUploadingImage(true)
|
||||||
|
const uploadFormData = new FormData()
|
||||||
|
uploadFormData.append('file', formData.imageFile)
|
||||||
|
|
||||||
|
const uploadResponse = await fetch('/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: uploadFormData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!uploadResponse.ok) {
|
||||||
|
const error = await uploadResponse.json()
|
||||||
|
alert(`Image upload failed: ${error.error}`)
|
||||||
|
setUploadingImage(false)
|
||||||
|
setSubmitting(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadData = await uploadResponse.json()
|
||||||
|
imageUrl = uploadData.url
|
||||||
|
setUploadingImage(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create drop
|
||||||
|
const response = await fetch('/api/drops', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
item: formData.item,
|
||||||
|
size: parseInt(formData.size),
|
||||||
|
unit: formData.unit.trim() || 'g',
|
||||||
|
ppu: parseFloat(formData.ppu),
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Reset form
|
||||||
|
setFormData({
|
||||||
|
item: '',
|
||||||
|
size: '',
|
||||||
|
unit: 'g',
|
||||||
|
ppu: '',
|
||||||
|
imageFile: null,
|
||||||
|
imagePreview: '',
|
||||||
|
})
|
||||||
|
// Clear file input
|
||||||
|
const fileInput = document.getElementById('imageFile') as HTMLInputElement
|
||||||
|
if (fileInput) fileInput.value = ''
|
||||||
|
// Refresh drops list
|
||||||
|
fetchDrops()
|
||||||
|
alert('Drop created successfully!')
|
||||||
|
} else {
|
||||||
|
const error = await response.json()
|
||||||
|
alert(`Error: ${error.error}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating drop:', error)
|
||||||
|
alert('Failed to create drop')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
setUploadingImage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProgressPercentage = (fill: number, size: number) => {
|
||||||
|
return Math.min((fill / size) * 100, 100).toFixed(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSoldOut = (fill: number, size: number) => {
|
||||||
|
return fill >= size
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-page">
|
||||||
|
<div className="container">
|
||||||
|
<div className="admin-header">
|
||||||
|
<h1>Admin Panel</h1>
|
||||||
|
<button onClick={() => router.push('/')}>View Site</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-grid">
|
||||||
|
{/* Create Drop Form */}
|
||||||
|
<div className="admin-section">
|
||||||
|
<h2>Create New Drop</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="admin-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="item">Product Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="item"
|
||||||
|
value={formData.item}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, item: e.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="e.g. Harlequin – Collective Drop"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-grid">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="size">Batch Size</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="size"
|
||||||
|
value={formData.size}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, size: e.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="1000"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="unit">Unit</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="unit"
|
||||||
|
value={formData.unit}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, unit: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="g, kg, ml, etc."
|
||||||
|
required
|
||||||
|
maxLength={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="ppu">Price Per Gram (CHF)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="ppu"
|
||||||
|
value={formData.ppu}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, ppu: e.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="2.50"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="imageFile">Product Image (optional)</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="imageFile"
|
||||||
|
accept="image/jpeg,image/jpg,image/png,image/webp"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
className="file-input"
|
||||||
|
/>
|
||||||
|
{formData.imagePreview && (
|
||||||
|
<div className="image-preview">
|
||||||
|
<img src={formData.imagePreview} alt="Preview" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="file-hint">
|
||||||
|
Max file size: 5MB. Allowed formats: JPEG, PNG, WebP
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting || uploadingImage}
|
||||||
|
className="cta"
|
||||||
|
style={{ width: '100%', marginTop: '20px' }}
|
||||||
|
>
|
||||||
|
{uploadingImage
|
||||||
|
? 'Uploading image...'
|
||||||
|
: submitting
|
||||||
|
? 'Creating...'
|
||||||
|
: 'Create Drop'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Drops List */}
|
||||||
|
<div className="admin-section">
|
||||||
|
<h2>All Drops</h2>
|
||||||
|
{loading ? (
|
||||||
|
<p style={{ color: 'var(--muted)' }}>Loading...</p>
|
||||||
|
) : !Array.isArray(drops) || drops.length === 0 ? (
|
||||||
|
<p style={{ color: 'var(--muted)' }}>No drops yet</p>
|
||||||
|
) : (
|
||||||
|
<div className="drops-list">
|
||||||
|
{drops.map((drop) => (
|
||||||
|
<div
|
||||||
|
key={drop.id}
|
||||||
|
className="drop-card"
|
||||||
|
style={{
|
||||||
|
background: isSoldOut(drop.fill, drop.size)
|
||||||
|
? 'var(--bg-soft)'
|
||||||
|
: 'var(--card)',
|
||||||
|
opacity: isSoldOut(drop.fill, drop.size) ? 0.7 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="drop-card-header">
|
||||||
|
<div className="drop-card-info">
|
||||||
|
<h3>{drop.item}</h3>
|
||||||
|
<p>
|
||||||
|
ID: {drop.id} · Created:{' '}
|
||||||
|
{new Date(drop.created_at).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{isSoldOut(drop.fill, drop.size) && (
|
||||||
|
<span className="sold-out-badge">Sold Out</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="drop-card-progress">
|
||||||
|
<div className="drop-card-progress-header">
|
||||||
|
<span style={{ color: 'var(--muted)' }}>
|
||||||
|
{drop.fill}
|
||||||
|
{drop.unit} / {drop.size}
|
||||||
|
{drop.unit}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{getProgressPercentage(drop.fill, drop.size)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="progress">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: `${getProgressPercentage(
|
||||||
|
drop.fill,
|
||||||
|
drop.size
|
||||||
|
)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="drop-card-price">
|
||||||
|
{drop.ppu.toFixed(2)} CHF / {drop.unit}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
21
app/api/drops/active/route.ts
Normal file
21
app/api/drops/active/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import pool from '@/lib/db'
|
||||||
|
|
||||||
|
// GET /api/drops/active - Get the currently active drop (not sold out)
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.execute(
|
||||||
|
'SELECT * FROM drops WHERE fill < size ORDER BY created_at DESC LIMIT 1'
|
||||||
|
)
|
||||||
|
|
||||||
|
const drops = rows as any[]
|
||||||
|
return NextResponse.json(drops[0] || null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching active drop:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch active drop' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
58
app/api/drops/route.ts
Normal file
58
app/api/drops/route.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import pool from '@/lib/db'
|
||||||
|
|
||||||
|
// GET /api/drops - Get all drops
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.execute(
|
||||||
|
'SELECT * FROM drops ORDER BY created_at DESC'
|
||||||
|
)
|
||||||
|
return NextResponse.json(rows)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching drops:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch drops' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/drops - Create a new drop
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { item, size, unit = 'g', ppu, imageUrl } = body
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!item || !size || !ppu) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Missing required fields: item, size, ppu' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new drop
|
||||||
|
// Note: If imageUrl column doesn't exist in database, add it with:
|
||||||
|
// ALTER TABLE drops ADD COLUMN image_url VARCHAR(255) DEFAULT NULL AFTER unit;
|
||||||
|
const [result] = await pool.execute(
|
||||||
|
'INSERT INTO drops (item, size, unit, ppu, fill, image_url) VALUES (?, ?, ?, ?, 0, ?)',
|
||||||
|
[item, size, unit, ppu, imageUrl || null]
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertId = (result as any).insertId
|
||||||
|
|
||||||
|
// Fetch the created drop
|
||||||
|
const [rows] = await pool.execute('SELECT * FROM drops WHERE id = ?', [
|
||||||
|
insertId,
|
||||||
|
])
|
||||||
|
|
||||||
|
return NextResponse.json(rows[0], { status: 201 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating drop:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to create drop' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
66
app/api/upload/route.ts
Normal file
66
app/api/upload/route.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { writeFile, mkdir } from 'fs/promises'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const formData = await request.formData()
|
||||||
|
const file = formData.get('file') as File
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'No file uploaded' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||||
|
if (!validTypes.includes(file.type)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (max 5MB)
|
||||||
|
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'File size too large. Maximum size is 5MB.' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await file.arrayBuffer()
|
||||||
|
const buffer = Buffer.from(bytes)
|
||||||
|
|
||||||
|
// Create uploads directory if it doesn't exist
|
||||||
|
const uploadsDir = join(process.cwd(), 'public', 'uploads')
|
||||||
|
if (!existsSync(uploadsDir)) {
|
||||||
|
await mkdir(uploadsDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const originalName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||||
|
const filename = `${timestamp}-${originalName}`
|
||||||
|
const filepath = join(uploadsDir, filename)
|
||||||
|
|
||||||
|
// Write file
|
||||||
|
await writeFile(filepath, buffer)
|
||||||
|
|
||||||
|
// Return the public URL
|
||||||
|
const fileUrl = `/uploads/${filename}`
|
||||||
|
|
||||||
|
return NextResponse.json({ url: fileUrl }, { status: 200 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading file:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to upload file' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
54
app/components/Drop.tsx
Normal file
54
app/components/Drop.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
export default function Drop() {
|
||||||
|
const [selectedSize, setSelectedSize] = useState('50g')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="drop">
|
||||||
|
<Image
|
||||||
|
src="https://images.unsplash.com/photo-1604908554027-0b6c2c9c7e92"
|
||||||
|
alt="Harlequin CBD"
|
||||||
|
width={420}
|
||||||
|
height={420}
|
||||||
|
style={{ width: '100%', height: 'auto', borderRadius: '16px', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2>Harlequin – Collective Drop</h2>
|
||||||
|
<div className="meta">1kg Batch · Indoor · Switzerland</div>
|
||||||
|
<div className="price">2.50 CHF / g · incl. 2.5% VAT</div>
|
||||||
|
|
||||||
|
<div className="progress">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div className="meta">620g of 1,000g reserved</div>
|
||||||
|
|
||||||
|
<div className="options">
|
||||||
|
<button
|
||||||
|
className={selectedSize === '50g' ? 'active' : ''}
|
||||||
|
onClick={() => setSelectedSize('50g')}
|
||||||
|
>
|
||||||
|
50g
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={selectedSize === '100g' ? 'active' : ''}
|
||||||
|
onClick={() => setSelectedSize('100g')}
|
||||||
|
>
|
||||||
|
100g
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={selectedSize === '250g' ? 'active' : ''}
|
||||||
|
onClick={() => setSelectedSize('250g')}
|
||||||
|
>
|
||||||
|
250g
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="cta">Join Drop</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
8
app/components/Footer.tsx
Normal file
8
app/components/Footer.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer>
|
||||||
|
© 2025 420Deals.ch · CBD < 1% THC · Sale from 18 years · Switzerland
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
28
app/components/InfoBox.tsx
Normal file
28
app/components/InfoBox.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export default function InfoBox() {
|
||||||
|
return (
|
||||||
|
<div className="info-box">
|
||||||
|
<div>
|
||||||
|
<h3>Why so cheap?</h3>
|
||||||
|
<p>
|
||||||
|
Retail prices are around 10 CHF/g. Through collective
|
||||||
|
bulk orders, we buy like wholesalers – without
|
||||||
|
intermediaries.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Taxes & Legal</h3>
|
||||||
|
<p>
|
||||||
|
Bulk sale with 2.5% VAT. No retail packaging, no
|
||||||
|
tobacco tax.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Drop Model</h3>
|
||||||
|
<p>
|
||||||
|
One variety per drop. Only when sold out – then the next drop.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
13
app/components/Nav.tsx
Normal file
13
app/components/Nav.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default function Nav() {
|
||||||
|
return (
|
||||||
|
<nav>
|
||||||
|
<div className="brand">420Deals.ch</div>
|
||||||
|
<div className="links">
|
||||||
|
<a href="#drop">Drop</a>
|
||||||
|
<a href="#past">Past Drops</a>
|
||||||
|
<a href="#community">Community</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
47
app/components/PastDrops.tsx
Normal file
47
app/components/PastDrops.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
interface PastDrop {
|
||||||
|
name: string
|
||||||
|
image: string
|
||||||
|
soldIn: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const pastDrops: PastDrop[] = [
|
||||||
|
{
|
||||||
|
name: 'Swiss Gold',
|
||||||
|
image: 'https://images.unsplash.com/photo-1581091012184-5c7b4c101899',
|
||||||
|
soldIn: 'Sold out in 42h',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lemon T1',
|
||||||
|
image: 'https://images.unsplash.com/photo-1512436991641-6745cdb1723f',
|
||||||
|
soldIn: 'Sold out in 19h',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Alpine Frost',
|
||||||
|
image: 'https://images.unsplash.com/photo-1600431521340-491eca880813',
|
||||||
|
soldIn: 'Sold out in 31h',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function PastDrops() {
|
||||||
|
return (
|
||||||
|
<div className="past">
|
||||||
|
{pastDrops.map((drop, index) => (
|
||||||
|
<div key={index} className="card">
|
||||||
|
<Image
|
||||||
|
src={drop.image}
|
||||||
|
alt={drop.name}
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
style={{ width: '100%', height: 'auto', borderRadius: '12px', marginBottom: '12px' }}
|
||||||
|
/>
|
||||||
|
<strong>{drop.name}</strong>
|
||||||
|
<br />
|
||||||
|
<span className="meta">{drop.soldIn}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
38
app/components/Signup.tsx
Normal file
38
app/components/Signup.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function Signup() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [whatsapp, setWhatsapp] = useState('')
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
// Handle form submission
|
||||||
|
console.log('Form submitted', { email, whatsapp })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="signup">
|
||||||
|
<h2>Drop Notifications</h2>
|
||||||
|
<p>Receive updates about new drops via email or WhatsApp.</p>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="E-Mail"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="WhatsApp Number"
|
||||||
|
value={whatsapp}
|
||||||
|
onChange={(e) => setWhatsapp(e.target.value)}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button type="submit">Get Notified</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
436
app/globals.css
Normal file
436
app/globals.css
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #0e0e0e;
|
||||||
|
--bg-soft: #151515;
|
||||||
|
--card: #1c1c1c;
|
||||||
|
--text: #eaeaea;
|
||||||
|
--muted: #9a9a9a;
|
||||||
|
--accent: #3ddc84;
|
||||||
|
--border: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(14, 14, 14, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 20px 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .brand {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .links a {
|
||||||
|
margin-left: 28px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .links a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 80px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding-top: 120px;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: 600;
|
||||||
|
max-width: 760px;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 620px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 420px 1fr;
|
||||||
|
gap: 50px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop .meta {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 62%;
|
||||||
|
background: linear-gradient(90deg, var(--accent), #1fa463);
|
||||||
|
}
|
||||||
|
|
||||||
|
.options button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 14px 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options button.active,
|
||||||
|
.options button:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 16px 28px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
margin-top: 60px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box div h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box div p {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 50px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup input {
|
||||||
|
width: 260px;
|
||||||
|
padding: 14px;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup button {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 14px 28px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.past {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.past .card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.past img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.drop {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Panel Styles */
|
||||||
|
.admin-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header button:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .file-input {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .file-input::file-selector-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .file-input::file-selector-button:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 200px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-hint {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 80px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drops-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card-info h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sold-out-badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card-progress {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card-progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card-progress-header span:last-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-card-price {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.admin-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
23
app/layout.tsx
Normal file
23
app/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'], weight: ['300', '400', '500', '600'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: '420Deals.ch – Premium Swiss CBD Drops',
|
||||||
|
description: 'Shop together. Wholesale prices for private buyers.',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
38
app/page.tsx
Normal file
38
app/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import Nav from './components/Nav'
|
||||||
|
import Drop from './components/Drop'
|
||||||
|
import InfoBox from './components/InfoBox'
|
||||||
|
import Signup from './components/Signup'
|
||||||
|
import PastDrops from './components/PastDrops'
|
||||||
|
import Footer from './components/Footer'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Nav />
|
||||||
|
<header className="container">
|
||||||
|
<h1>Shop together. Wholesale prices for private buyers.</h1>
|
||||||
|
<p>
|
||||||
|
Limited CBD drops directly from Swiss producers. No retail.
|
||||||
|
No markup. Just collective bulk prices.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="container" id="drop">
|
||||||
|
<Drop />
|
||||||
|
<InfoBox />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="container" id="community">
|
||||||
|
<Signup />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="container" id="past">
|
||||||
|
<h2>Past Drops</h2>
|
||||||
|
<PastDrops />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
159
cbd420.sql
Normal file
159
cbd420.sql
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
-- phpMyAdmin SQL Dump
|
||||||
|
-- version 5.2.1deb1+deb12u1
|
||||||
|
-- https://www.phpmyadmin.net/
|
||||||
|
--
|
||||||
|
-- Host: localhost:3306
|
||||||
|
-- Generation Time: Dec 20, 2025 at 04:42 AM
|
||||||
|
-- Server version: 10.11.14-MariaDB-0+deb12u2
|
||||||
|
-- PHP Version: 8.2.29
|
||||||
|
|
||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET time_zone = "+00:00";
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Database: `cbd420`
|
||||||
|
--
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `buyers`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `buyers` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`username` varchar(255) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`email` varchar(255) NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `deliveries`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `deliveries` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`sale_id` int(11) NOT NULL,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`status` text NOT NULL DEFAULT 'Pending'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `drops`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `drops` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`item` text NOT NULL,
|
||||||
|
`size` int(11) NOT NULL DEFAULT 100,
|
||||||
|
`fill` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`unit` varchar(12) NOT NULL DEFAULT 'g',
|
||||||
|
`ppu` int(11) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `sales`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `sales` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`drop_id` int(11) NOT NULL,
|
||||||
|
`buyer_id` int(11) NOT NULL,
|
||||||
|
`size` int(11) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `buyers`
|
||||||
|
--
|
||||||
|
ALTER TABLE `buyers`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `deliveries`
|
||||||
|
--
|
||||||
|
ALTER TABLE `deliveries`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `sale_id` (`sale_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `drops`
|
||||||
|
--
|
||||||
|
ALTER TABLE `drops`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `sales`
|
||||||
|
--
|
||||||
|
ALTER TABLE `sales`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `drop_id` (`drop_id`),
|
||||||
|
ADD KEY `buyer_id` (`buyer_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `buyers`
|
||||||
|
--
|
||||||
|
ALTER TABLE `buyers`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `deliveries`
|
||||||
|
--
|
||||||
|
ALTER TABLE `deliveries`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `drops`
|
||||||
|
--
|
||||||
|
ALTER TABLE `drops`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `sales`
|
||||||
|
--
|
||||||
|
ALTER TABLE `sales`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `deliveries`
|
||||||
|
--
|
||||||
|
ALTER TABLE `deliveries`
|
||||||
|
ADD CONSTRAINT `deliveries_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `sales` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `sales`
|
||||||
|
--
|
||||||
|
ALTER TABLE `sales`
|
||||||
|
ADD CONSTRAINT `sales_ibfk_1` FOREIGN KEY (`drop_id`) REFERENCES `drops` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT `sales_ibfk_2` FOREIGN KEY (`buyer_id`) REFERENCES `buyers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
287
index.html
Normal file
287
index.html
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>420Deals.ch – Premium Swiss CBD Drops</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0e0e0e;
|
||||||
|
--bg-soft: #151515;
|
||||||
|
--card: #1c1c1c;
|
||||||
|
--text: #eaeaea;
|
||||||
|
--muted: #9a9a9a;
|
||||||
|
--accent: #3ddc84;
|
||||||
|
--border: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
|
nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(14,14,14,0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 20px 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .brand { font-weight: 600; font-size: 18px; letter-spacing: 0.5px; }
|
||||||
|
nav .links a { margin-left: 28px; font-size: 14px; color: var(--muted); }
|
||||||
|
nav .links a:hover { color: var(--text); }
|
||||||
|
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; padding: 80px 20px; }
|
||||||
|
|
||||||
|
header { padding-top: 120px; padding-bottom: 80px; }
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: 600;
|
||||||
|
max-width: 760px;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 620px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 420px 1fr;
|
||||||
|
gap: 50px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop h2 { font-size: 28px; margin: 0 0 10px; }
|
||||||
|
.drop .meta { color: var(--muted); margin-bottom: 20px; }
|
||||||
|
.price { font-size: 22px; font-weight: 500; margin-bottom: 20px; }
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 62%;
|
||||||
|
background: linear-gradient(90deg, var(--accent), #1fa463);
|
||||||
|
}
|
||||||
|
|
||||||
|
.options button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 14px 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options button.active,
|
||||||
|
.options button:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 16px 28px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
margin-top: 60px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px,1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box div h3 { margin-bottom: 8px; font-size: 18px; }
|
||||||
|
.info-box div p { color: var(--muted); font-size: 14px; }
|
||||||
|
|
||||||
|
.signup {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 50px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup input {
|
||||||
|
width: 260px;
|
||||||
|
padding: 14px;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup button {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 14px 28px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.past {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px,1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.past .card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.past img { width: 100%; border-radius: 12px; margin-bottom: 12px; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width:900px) {
|
||||||
|
.drop { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<div class="brand">420Deals.ch</div>
|
||||||
|
<div class="links">
|
||||||
|
<a href="#drop">Drop</a>
|
||||||
|
<a href="#past">Vergangene Drops</a>
|
||||||
|
<a href="#community">Community</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="container">
|
||||||
|
<h1>Gemeinsam einkaufen. Wholesale-Preise für private Käufer.</h1>
|
||||||
|
<p>Limitierte CBD Drops direkt von Schweizer Produzenten. Kein Retail. Kein Marketing-Aufschlag. Nur kollektive Mengenpreise.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="container" id="drop">
|
||||||
|
<div class="drop">
|
||||||
|
<img src="https://images.unsplash.com/photo-1604908554027-0b6c2c9c7e92" />
|
||||||
|
<div>
|
||||||
|
<h2>Harlequin – Collective Drop</h2>
|
||||||
|
<div class="meta">1kg Batch · Indoor · Schweiz</div>
|
||||||
|
<div class="price">2.50 CHF / g · inkl. 2.5% MWST</div>
|
||||||
|
|
||||||
|
<div class="progress"><span></span></div>
|
||||||
|
<div class="meta">620g von 1'000g reserviert</div>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
<button class="active">50g</button>
|
||||||
|
<button>100g</button>
|
||||||
|
<button>250g</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="cta">Am Drop teilnehmen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<div>
|
||||||
|
<h3>Warum so günstig?</h3>
|
||||||
|
<p>Retailpreise liegen bei ca. 10 CHF/g. Durch kollektive Sammelbestellungen kaufen wir wie Grosshändler ein – ohne Zwischenstufen.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Steuern & Recht</h3>
|
||||||
|
<p>Bulk-Verkauf mit 2.5% MWST. Keine Retail-Verpackung, keine Tabaksteuer.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Drop-Modell</h3>
|
||||||
|
<p>Pro Drop nur eine Sorte. Erst ausverkauft – dann der nächste Drop.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container" id="community">
|
||||||
|
<div class="signup">
|
||||||
|
<h2>Drop-Benachrichtigungen</h2>
|
||||||
|
<p>Erhalte Updates zu neuen Drops per E-Mail oder WhatsApp.</p>
|
||||||
|
<input type="email" placeholder="E-Mail" />
|
||||||
|
<input type="text" placeholder="WhatsApp Nummer" />
|
||||||
|
<br />
|
||||||
|
<button>Benachrichtigen lassen</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container" id="past">
|
||||||
|
<h2>Vergangene Drops</h2>
|
||||||
|
<div class="past">
|
||||||
|
<div class="card">
|
||||||
|
<img src="https://images.unsplash.com/photo-1581091012184-5c7b4c101899" />
|
||||||
|
<strong>Swiss Gold</strong><br><span class="meta">Ausverkauft in 42h</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<img src="https://images.unsplash.com/photo-1512436991641-6745cdb1723f" />
|
||||||
|
<strong>Lemon T1</strong><br><span class="meta">Ausverkauft in 19h</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<img src="https://images.unsplash.com/photo-1600431521340-491eca880813" />
|
||||||
|
<strong>Alpine Frost</strong><br><span class="meta">Ausverkauft in 31h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
© 2025 420Deals.ch · CBD < 1% THC · Verkauf ab 18 Jahren · Schweiz
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
lib/db.ts
Normal file
15
lib/db.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || 'cbd420',
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default pool
|
||||||
|
|
||||||
6
migrations/add_image_url.sql
Normal file
6
migrations/add_image_url.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Migration: Add image_url column to drops table
|
||||||
|
-- Run this to add support for image uploads
|
||||||
|
|
||||||
|
ALTER TABLE `drops`
|
||||||
|
ADD COLUMN `image_url` VARCHAR(255) DEFAULT NULL AFTER `unit`;
|
||||||
|
|
||||||
15
next.config.js
Normal file
15
next.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'images.unsplash.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
|
||||||
621
package-lock.json
generated
Normal file
621
package-lock.json
generated
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
{
|
||||||
|
"name": "420deals-ch",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "420deals-ch",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"mysql2": "^3.16.0",
|
||||||
|
"next": "^14.2.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/env": {
|
||||||
|
"version": "14.2.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
|
||||||
|
"integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
|
"version": "14.2.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
|
||||||
|
"integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/counter": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
|
||||||
|
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/counter": "^0.1.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||||
|
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.3.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||||
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "18.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-ssl-profiles": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/busboy": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
|
"dependencies": {
|
||||||
|
"streamsearch": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/caniuse-lite": {
|
||||||
|
"version": "1.0.30001761",
|
||||||
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
|
||||||
|
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "CC-BY-4.0"
|
||||||
|
},
|
||||||
|
"node_modules/client-only": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/generate-function": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-property": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-property": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru.min": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=1.0.0",
|
||||||
|
"deno": ">=1.30.0",
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wellwelwel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mysql2": {
|
||||||
|
"version": "3.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz",
|
||||||
|
"integrity": "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-ssl-profiles": "^1.1.1",
|
||||||
|
"denque": "^2.1.0",
|
||||||
|
"generate-function": "^2.3.1",
|
||||||
|
"iconv-lite": "^0.7.0",
|
||||||
|
"long": "^5.2.1",
|
||||||
|
"lru.min": "^1.0.0",
|
||||||
|
"named-placeholders": "^1.1.3",
|
||||||
|
"seq-queue": "^0.0.5",
|
||||||
|
"sqlstring": "^2.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/named-placeholders": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lru.min": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/next": {
|
||||||
|
"version": "14.2.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
|
||||||
|
"integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@next/env": "14.2.35",
|
||||||
|
"@swc/helpers": "0.5.5",
|
||||||
|
"busboy": "1.6.0",
|
||||||
|
"caniuse-lite": "^1.0.30001579",
|
||||||
|
"graceful-fs": "^4.2.11",
|
||||||
|
"postcss": "8.4.31",
|
||||||
|
"styled-jsx": "5.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"next": "dist/bin/next"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@next/swc-darwin-arm64": "14.2.33",
|
||||||
|
"@next/swc-darwin-x64": "14.2.33",
|
||||||
|
"@next/swc-linux-arm64-gnu": "14.2.33",
|
||||||
|
"@next/swc-linux-arm64-musl": "14.2.33",
|
||||||
|
"@next/swc-linux-x64-gnu": "14.2.33",
|
||||||
|
"@next/swc-linux-x64-musl": "14.2.33",
|
||||||
|
"@next/swc-win32-arm64-msvc": "14.2.33",
|
||||||
|
"@next/swc-win32-ia32-msvc": "14.2.33",
|
||||||
|
"@next/swc-win32-x64-msvc": "14.2.33"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.1.0",
|
||||||
|
"@playwright/test": "^1.41.2",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"sass": "^1.3.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@playwright/test": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.4.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
|
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.6",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/scheduler": {
|
||||||
|
"version": "0.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/seq-queue": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||||
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sqlstring": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/streamsearch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/styled-jsx": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"client-only": "0.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@babel/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"babel-plugin-macros": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "420deals-ch",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mysql2": "^3.16.0",
|
||||||
|
"next": "^14.2.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user