chore(dev): Dummy Hedgebox app (#39189)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Thomas Obermüller <thomas.obermueller@gmail.com>
This commit is contained in:
Michael Matloka
2025-10-10 13:41:08 +02:00
committed by GitHub
parent ddb852bed6
commit 7e320661dd
29 changed files with 6413 additions and 17 deletions

View File

@@ -124,5 +124,9 @@ procs:
shell: 'pnpm --filter=@posthog/storybook install && pnpm run storybook'
autostart: false
hedgebox-dummy:
shell: 'bin/check_postgres_up && cd hedgebox-dummy && pnpm install && pnpm run dev'
autostart: false
mouse_scroll_speed: 1
scrollback: 10000

3
hedgebox-dummy/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.next
.env.local
node_modules

64
hedgebox-dummy/README.md Normal file
View File

@@ -0,0 +1,64 @@
# Hedgebox dummy app
This app represents our Hedgebox product simulation for demo data purposes.
## Background
We've had a [demo data generator](../posthog/demo/products/hedgebox/) simulating a product called Hedgebox (like Dropbox for hedgehogs) for a while now. It creates realistic event data with user profiles, behaviors, timezones, even includes features like a Marius Tech Tips sponsorship landing page and an A/B test on the signup flow.
However, the generator hasn't been able to create session recording data, as that's a hard nut to ~~crack~~ store. Session recordings need an actual app with actual user interactions to capture. Hedgebox never had one - until now.
This dummy app brings Hedgebox to life as a working Next.js app that can be used to generate real session recordings for demos and testing.
## What's Inside
A fully functional (fake) demo app with:
- Login/signup flows
- File management interface
- Pricing page
- Marius Tech Tips landing page
- PostHog integration for tracking and session recording
## Setup
1. Install dependencies:
```bash
npm install
```
2. Set up PostHog environment variables:
The app automatically fetches the PostHog API key from your local database at build/dev time. You can configure the database connection and team ID using these environment variables:
```env
NEXT_PUBLIC_POSTHOG_HOST # PostHog host (default: http://localhost:8010)
NEXT_PUBLIC_POSTHOG_KEY # PostHog API key, fetched automatically on `npm run dev`
DEMO_TEAM_ID # Team ID to fetch token from (default: latest team)
```
**Note:** The API key is automatically fetched and written to `.env.local` when you run `npm run dev` or `npm run build`. The script will skip fetching if `.env.local` already exists (to avoid unnecessary database queries on every run).
To manually fetch the key or force a re-fetch, run:
```bash
# Fetch if .env.local doesn't exist or doesn't have key NEXT_PUBLIC_POSTHOG_KEY
npm run fetch-posthog-key
# Force re-fetch even if NEXT_PUBLIC_POSTHOG_KEY set in .env.local
FORCE_FETCH_KEY=1 npm run fetch-posthog-key
```
Alternatively, you can manually create a `.env.local` file with the `NEXT_*` vars above.
3. Run the development server:
```bash
npm run dev
```
The app will be available at [http://localhost:3000](http://localhost:3000).
## Generating session recordings
The app is instrumented as long as `npm run dev` has `NEXT_PUBLIC_POSTHOG_KEY` set. Just interact normally and recordings will be captured in your PostHog instance.

5
hedgebox-dummy/next-env.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,34 @@
{
"name": "hedgebox-demo-app",
"version": "0.1.0",
"private": true,
"scripts": {
"fetch-posthog-key": "node scripts/fetch-posthog-key.js",
"dev": "pnpm run fetch-posthog-key && next dev",
"build": "pnpm run fetch-posthog-key && next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.0.0",
"posthog-js": "~1.250.0",
"react": "^18",
"react-dom": "^18",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^9.0.0",
"autoprefixer": "^10.0.1",
"daisyui": "^4.4.0",
"eslint": "^8",
"eslint-config-next": "14.0.0",
"pg": "^8.16.3",
"playwright": "^1.40.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

3940
hedgebox-dummy/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

View File

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

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env node
const { Client } = require('pg')
const fs = require('fs')
const path = require('path')
async function fetchPostHogKey() {
const envPath = path.join(__dirname, '..', '.env.local')
// Check if .env.local already exists with a valid key
if (fs.existsSync(envPath) && !process.env.FORCE_FETCH_KEY) {
const envContent = fs.readFileSync(envPath, 'utf-8')
const keyMatch = envContent.match(/NEXT_PUBLIC_POSTHOG_KEY=(.+)/)
if (keyMatch && keyMatch[1] && keyMatch[1].trim()) {
console.info('✓ PostHog API key already exists in .env.local')
console.info(' `rm hedgebox-dummy/.env.local` or `export FORCE_FETCH_KEY=1` to fetch anew\n')
return
}
}
const client = new Client({
host: process.env.PGHOST || 'localhost',
port: parseInt(process.env.PGPORT || '5432'),
database: process.env.PGDATABASE || 'posthog',
user: process.env.PGUSER || 'posthog',
password: process.env.PGPASSWORD || 'posthog',
connectionTimeoutMillis: 3000, // A brief timeout to avoid waiting forever
})
try {
await client.connect()
// Query for the latest team's API token (or you can specify a team_id)
const result = await client.query(
process.env.DEMO_TEAM_ID
? 'SELECT id, api_token FROM posthog_team WHERE id = $1'
: 'SELECT id, api_token FROM posthog_team ORDER BY created_at DESC LIMIT 1',
process.env.DEMO_TEAM_ID ? [process.env.DEMO_TEAM_ID] : []
)
if (result.rows.length === 0) {
console.warn(`${process.env.DEMO_TEAM_ID ? `No team found with ID ${process.env.DEMO_TEAM_ID}` : 'No team found'}`)
console.warn(' Continuing without PostHog API key...\n')
return
}
const { id: teamId, api_token: apiToken } = result.rows[0]
// Write to .env.local file
const envPath = path.join(__dirname, '..', '.env.local')
const envContent = `NEXT_PUBLIC_POSTHOG_KEY=${apiToken}\n`
fs.writeFileSync(envPath, envContent)
console.info(`✓ PostHog API key fetched and written to .env.local`)
console.info(` Team ID: ${teamId}`)
console.info(` API token: ${apiToken}\n`)
} catch (error) {
console.warn('⚠ Error fetching PostHog key:', error.message)
console.warn(' Continuing without PostHog API key...\n')
} finally {
await client.end()
}
}
fetchPostHogKey()

View File

@@ -0,0 +1,322 @@
'use client'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import Header from '@/components/Header'
import { useAuth } from '@/lib/auth'
import { sampleFiles } from '@/lib/data'
import { useAuthRedirect } from '@/lib/hooks'
import { posthog } from '@/lib/posthog'
import { formatFileSize, getFileIcon } from '@/lib/utils'
import { HedgeboxFile } from '@/types'
interface FilePageProps {
params: {
id: string
}
}
export default function FilePage({ params }: FilePageProps): React.JSX.Element | null {
const { user } = useAuth()
const [file, setFile] = useState<HedgeboxFile | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [showShareModal, setShowShareModal] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const router = useRouter()
useAuthRedirect()
useEffect(() => {
// Simulate finding the file by ID
const foundFile = sampleFiles.find((f) => f.id === params.id)
setFile(foundFile || null)
setIsLoading(false)
// Track file view
if (foundFile) {
posthog.capture('viewed_file', {
file_id: foundFile.id,
file_type: foundFile.type,
file_size_b: foundFile.size,
})
}
}, [params.id])
if (!user) {return null}
if (isLoading) {
return (
<div>
<Header />
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="flex items-center justify-center py-20">
<span className="loading loading-spinner loading-lg" />
</div>
</div>
</div>
)
}
if (!file) {
return (
<div>
<Header />
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="text-center py-20">
<div className="text-8xl mb-4">🔍</div>
<h2 className="text-3xl font-bold mb-4">File not found</h2>
<p className="text-base-content/70 mb-6">
The file you're looking for doesn't exist or has been deleted.
</p>
<Link href="/files" className="btn btn-primary">
Back to Files
</Link>
</div>
</div>
</div>
)
}
const handleDownload = (): void => {
setIsProcessing(true)
posthog.capture('downloaded_file', {
file_id: file.id,
file_type: file.type,
file_size_b: file.size,
})
// Simulate download processing
setTimeout(() => {
setIsProcessing(false)
// In a real app, this would trigger the actual download
}, 2000)
}
const handleShare = (): void => {
posthog.capture('shared_file', {
file_id: file.id,
file_type: file.type,
file_size_b: file.size,
})
setShowShareModal(true)
}
const handleDelete = (): void => {
if (confirm('Are you sure you want to delete this file? This action cannot be undone.')) {
posthog.capture('deleted_file', {
file_id: file.id,
file_type: file.type,
file_size_b: file.size,
})
router.push('/files')
}
}
const copyShareLink = (): void => {
const shareLink = file.sharedLink || `https://hedgebox.net/files/${file.id}/shared`
navigator.clipboard.writeText(shareLink)
posthog.capture('copied_share_link', {
file_id: file.id,
})
}
const getPreviewComponent = (): React.JSX.Element => {
if (file.type.startsWith('image/')) {
return (
<div className="bg-base-200 rounded-lg p-8 text-center">
<div className="text-8xl mb-4">{getFileIcon(file.type)}</div>
<p className="text-base-content/70">Image preview would appear here</p>
</div>
)
} else if (file.type === 'application/pdf') {
return (
<div className="bg-base-200 rounded-lg p-8 text-center">
<div className="text-8xl mb-4">{getFileIcon(file.type)}</div>
<p className="text-base-content/70">PDF preview would appear here</p>
</div>
)
} else if (file.type.startsWith('video/')) {
return (
<div className="bg-base-200 rounded-lg p-8 text-center">
<div className="text-8xl mb-4">{getFileIcon(file.type)}</div>
<p className="text-base-content/70">Video preview would appear here</p>
</div>
)
}
return (
<div className="bg-base-200 rounded-lg p-8 text-center">
<div className="text-8xl mb-4">{getFileIcon(file.type)}</div>
<p className="text-base-content/70">No preview available for this file type</p>
</div>
)
}
return (
<div>
<Header />
<div className="container mx-auto px-4 py-8 max-w-4xl">
{/* Breadcrumb */}
<div className="breadcrumbs text-sm mb-6">
<ul>
<li>
<Link href="/files" className="link link-hover">
📁 Files
</Link>
</li>
<li className="text-base-content/70">{file.name}</li>
</ul>
</div>
{/* File Header */}
<div className="card bg-base-100 shadow-lg mb-8">
<div className="card-body">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-4">
<div className="text-6xl">{getFileIcon(file.type)}</div>
<div>
<h1 className="text-3xl font-bold mb-2">{file.name}</h1>
<div className="flex items-center space-x-4 text-base-content/70">
<span>{formatFileSize(file.size)}</span>
<span></span>
<span>{file.type}</span>
<span></span>
<span>Uploaded {new Date(file.uploadedAt).toLocaleDateString()}</span>
</div>
{file.sharedLink && <div className="badge badge-secondary mt-2">🔗 Shared</div>}
</div>
</div>
<div className="flex space-x-2">
<button
onClick={handleDownload}
className={`btn btn-primary ${isProcessing ? 'loading' : ''}`}
disabled={isProcessing}
>
{isProcessing ? 'Processing...' : '📥 Download'}
</button>
<button onClick={handleShare} className="btn btn-secondary">
🔗 Share
</button>
<div className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost">
</div>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-lg w-40"
>
<li>
<Link href="/files">📁 Back to Files</Link>
</li>
<li>
<button onClick={handleDelete} className="text-error">
🗑 Delete
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{/* File Preview */}
<div className="card bg-base-100 shadow-lg mb-8">
<div className="card-body">
<h2 className="text-xl font-bold mb-4">Preview</h2>
{getPreviewComponent()}
</div>
</div>
{/* File Details */}
<div className="card bg-base-100 shadow-lg">
<div className="card-body">
<h2 className="text-xl font-bold mb-4">File Details</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="stat">
<div className="stat-title">File Name</div>
<div className="stat-value text-lg">{file.name}</div>
</div>
</div>
<div>
<div className="stat">
<div className="stat-title">File Size</div>
<div className="stat-value text-lg">{formatFileSize(file.size)}</div>
</div>
</div>
<div>
<div className="stat">
<div className="stat-title">File Type</div>
<div className="stat-value text-lg">{file.type}</div>
</div>
</div>
<div>
<div className="stat">
<div className="stat-title">Upload Date</div>
<div className="stat-value text-lg">
{new Date(file.uploadedAt).toLocaleDateString()}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Share Modal */}
{showShareModal && (
<div className="modal modal-open">
<div className="modal-box">
<h3 className="font-bold text-lg mb-4">Share File</h3>
<p className="mb-4">Share this file with others using the link below:</p>
<div className="form-control mb-4">
<label className="label">
<span className="label-text">Share Link</span>
</label>
<div className="join">
<input
type="text"
value={file.sharedLink || `https://hedgebox.net/files/${file.id}/shared`}
className="input input-bordered join-item flex-1"
readOnly
/>
<button onClick={copyShareLink} className="btn btn-primary join-item">
📋 Copy
</button>
</div>
</div>
<div className="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="stroke-current shrink-0 w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>Anyone with this link can view and download this file.</span>
</div>
<div className="modal-action">
<button onClick={() => setShowShareModal(false)} className="btn">
Close
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,397 @@
'use client'
import Link from 'next/link'
import { useState } from 'react'
import Header from '@/components/Header'
import { useAuth } from '@/lib/auth'
import { sampleFiles } from '@/lib/data'
import { useAuthRedirect } from '@/lib/hooks'
import { posthog } from '@/lib/posthog'
import { formatFileSize, getFileIcon } from '@/lib/utils'
import { HedgeboxFile } from '@/types'
export default function FilesPage(): React.JSX.Element {
const { user } = useAuth()
const [files, setFiles] = useState<HedgeboxFile[]>(sampleFiles)
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set())
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [isUploading, setIsUploading] = useState(false)
useAuthRedirect()
if (!user) {return null}
const handleFileUpload = async (): Promise<void> => {
setIsUploading(true)
const fileSize = Math.floor(Math.random() * 5000000)
posthog.capture('uploaded_file', {
file_type: 'image/jpeg',
file_size_b: fileSize,
used_mb: Math.floor((usedStorage + fileSize) / 1000000),
})
await new Promise((resolve) => setTimeout(resolve, 2000))
const newFile: HedgeboxFile = {
id: `file_${Date.now()}`,
name: `hedgehog-adventure-${Date.now()}.jpg`,
type: 'image/jpeg',
size: fileSize,
uploadedAt: new Date(),
}
setFiles((prev) => [newFile, ...prev])
setIsUploading(false)
}
const trackFileAction = (action: string, file: HedgeboxFile): void => {
posthog.capture(`${action}_file`, {
file_type: file.type,
file_size_b: file.size,
})
}
const handleFileDelete = (fileId: string): void => {
const file = files.find((f) => f.id === fileId)
if (!file) {return}
trackFileAction('deleted', file)
setFiles((prev) => prev.filter((f) => f.id !== fileId))
setSelectedFiles((prev) => {
const newSet = new Set(prev)
newSet.delete(fileId)
return newSet
})
}
const handleFileShare = (fileId: string): void => {
const file = files.find((f) => f.id === fileId)
if (!file) {return}
trackFileAction('shared', file)
setFiles((prev) =>
prev.map((f) => (f.id === fileId ? { ...f, sharedLink: `https://hedgebox.net/files/${fileId}/shared` } : f))
)
}
const handleFileDownload = (file: HedgeboxFile): void => {
trackFileAction('downloaded', file)
}
const toggleFileSelection = (fileId: string): void => {
setSelectedFiles((prev) => {
const newSet = new Set(prev)
if (newSet.has(fileId)) {
newSet.delete(fileId)
} else {
newSet.add(fileId)
}
return newSet
})
}
const usedStorage = files.reduce((total, file) => total + file.size, 0)
const maxStorage = 1000000000 // 1GB
const storagePercentage = (usedStorage / maxStorage) * 100
const getStorageProgressClass = (): string => {
if (storagePercentage > 90) {return 'progress-error'}
if (storagePercentage > 70) {return 'progress-warning'}
return 'progress-primary'
}
return (
<div>
<Header />
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Welcome Header */}
<div className="mb-8 animate-fade-in">
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h1 className="text-4xl font-bold mb-2">Welcome back, {user.name.split(' ')[0]}! 🦔</h1>
<p className="text-base-content/70 text-lg">
Manage your hedgehog files with spike-proof security
</p>
</div>
<div className="stats shadow">
<div className="stat">
<div className="stat-title">Total files</div>
<div className="stat-value text-primary">{files.length}</div>
</div>
<div className="stat">
<div className="stat-title">Storage used</div>
<div className="stat-value text-secondary">{formatFileSize(usedStorage)}</div>
<div className="stat-desc">{storagePercentage.toFixed(1)}% of 1GB</div>
</div>
</div>
</div>
{/* Storage Progress */}
<div className="mt-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Storage usage</span>
<span className="text-sm text-base-content/70">{formatFileSize(usedStorage)} / 1GB</span>
</div>
<progress
className={`progress w-full ${getStorageProgressClass()}`}
value={storagePercentage}
max="100"
/>
</div>
</div>
{/* Action Bar */}
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
<div className="flex items-center space-x-4">
<button
onClick={handleFileUpload}
className={`btn btn-primary ${isUploading ? 'loading' : ''}`}
disabled={isUploading}
>
{isUploading ? 'Uploading...' : '📤 Upload file'}
</button>
{selectedFiles.size > 0 && (
<div className="flex items-center space-x-2">
<span className="text-sm text-base-content/70">{selectedFiles.size} selected</span>
<button
className="btn btn-error btn-sm"
onClick={() => {
selectedFiles.forEach((fileId) => handleFileDelete(fileId))
}}
>
🗑 Delete
</button>
<button
className="btn btn-secondary btn-sm"
onClick={() => {
selectedFiles.forEach((fileId) => handleFileShare(fileId))
}}
>
📤 Share
</button>
</div>
)}
</div>
<div className="flex items-center space-x-2">
<div className="join">
<button
className={`btn btn-sm join-item ${viewMode === 'grid' ? 'btn-active' : ''}`}
onClick={() => setViewMode('grid')}
>
🔢 Grid
</button>
<button
className={`btn btn-sm join-item ${viewMode === 'list' ? 'btn-active' : ''}`}
onClick={() => setViewMode('list')}
>
🗒 List
</button>
</div>
</div>
</div>
{/* Files Display */}
{files.length === 0 ? (
<div className="text-center py-20 animate-fade-in">
<div className="text-8xl mb-4">📁</div>
<h3 className="text-2xl font-bold mb-2">No files yet</h3>
<p className="text-base-content/70 mb-6">Upload your first hedgehog files to get started!</p>
<button onClick={handleFileUpload} className="btn btn-primary btn-lg">
📤 Upload Your First File
</button>
</div>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 animate-fade-in">
{files.map((file) => (
<Link
key={file.id}
href={`/files/${file.id}`}
className={`card bg-base-100 shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group ${
selectedFiles.has(file.id) ? 'ring-2 ring-primary' : ''
}`}
>
<div className="card-body p-4">
<div className="flex items-start justify-between mb-3">
<div className="text-4xl">{getFileIcon(file.type)}</div>
<div className="flex items-center space-x-1">
<input
type="checkbox"
className="checkbox checkbox-primary checkbox-sm"
checked={selectedFiles.has(file.id)}
onChange={(e) => {
e.stopPropagation()
toggleFileSelection(file.id)
}}
/>
<div className="dropdown dropdown-end">
<div
tabIndex={0}
role="button"
className="btn btn-ghost btn-xs"
onClick={(e) => e.stopPropagation()}
>
</div>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-lg w-40"
>
<li>
<Link href={`/files/${file.id}`}>👁 View</Link>
</li>
<li>
<button
onClick={(e) => {
e.stopPropagation()
handleFileDownload(file)
}}
>
📥 Download
</button>
</li>
<li>
<button
onClick={(e) => {
e.stopPropagation()
handleFileShare(file.id)
}}
>
🔗 Share
</button>
</li>
<li>
<button
onClick={(e) => {
e.stopPropagation()
handleFileDelete(file.id)
}}
className="text-error"
>
🗑 Delete
</button>
</li>
</ul>
</div>
</div>
</div>
<h3 className="font-semibold text-sm truncate mb-2" title={file.name}>
{file.name}
</h3>
<div className="text-xs text-base-content/70 space-y-1">
<div>{formatFileSize(file.size)}</div>
<div>{new Date(file.uploadedAt).toLocaleDateString()}</div>
{file.sharedLink && (
<div className="badge badge-secondary badge-xs">Shared</div>
)}
</div>
</div>
</Link>
))}
</div>
) : (
<div className="overflow-x-auto animate-fade-in">
<table className="table table-zebra">
<thead>
<tr>
<th>
<input
type="checkbox"
className="checkbox checkbox-primary"
onChange={(e) => {
if (e.target.checked) {
setSelectedFiles(new Set(files.map((f) => f.id)))
} else {
setSelectedFiles(new Set())
}
}}
/>
</th>
<th>Name</th>
<th>Size</th>
<th>Modified</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr key={file.id} className="hover">
<td>
<input
type="checkbox"
className="checkbox checkbox-primary"
checked={selectedFiles.has(file.id)}
onChange={() => toggleFileSelection(file.id)}
/>
</td>
<td>
<Link
href={`/files/${file.id}`}
className="flex items-center space-x-3 cursor-pointer"
>
<div className="text-2xl">{getFileIcon(file.type)}</div>
<div>
<div className="font-bold hover:text-primary">{file.name}</div>
<div className="text-sm text-base-content/70">{file.type}</div>
</div>
</Link>
</td>
<td>{formatFileSize(file.size)}</td>
<td>{new Date(file.uploadedAt).toLocaleDateString()}</td>
<td>
{file.sharedLink ? (
<div className="badge badge-secondary">Shared</div>
) : (
<div className="badge badge-ghost">Private</div>
)}
</td>
<td>
<div className="flex space-x-2">
<Link
href={`/files/${file.id}`}
className="btn btn-ghost btn-xs"
title="View file"
>
👁
</Link>
<button
onClick={() => handleFileDownload(file)}
className="btn btn-ghost btn-xs"
title="Download file"
>
📥
</button>
<button
onClick={() => handleFileShare(file.id)}
className="btn btn-ghost btn-xs"
title="Share file"
>
🔗
</button>
<button
onClick={() => handleFileDelete(file.id)}
className="btn btn-ghost btn-xs text-error"
title="Delete file"
>
🗑
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,116 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* stylelint-disable keyframes-name-pattern */
/* Modern typography */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif;
font-variation-settings: normal;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Modern animations */
@layer utilities {
.animate-fade-in-up {
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-fade-in {
animation: fadeIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-scale-in {
animation: scaleIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-slide-up {
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
/* Gradient backgrounds */
.gradient-subtle {
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Modern scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--bc) / 20%);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--bc) / 30%);
}
/* Focus rings */
*:focus-visible {
outline: 2px solid hsl(var(--p));
outline-offset: 2px;
}
/* Selection */
::selection {
color: hsl(var(--pc));
background: hsl(var(--p) / 20%);
}

View File

@@ -0,0 +1,23 @@
'use client'
import './globals.css'
import { AuthProvider } from '@/lib/auth'
import { initPostHog } from '@/lib/posthog'
import { useEffect } from 'react'
export default function RootLayout({ children }: { children: React.ReactNode }): React.JSX.Element {
useEffect(() => {
initPostHog()
}, [])
return (
<html lang="en" data-theme="hedgebox">
<body>
<AuthProvider>
<div className="min-h-screen bg-base-100">{children}</div>
</AuthProvider>
</body>
</html>
)
}

View File

@@ -0,0 +1,120 @@
'use client'
import Header from '@/components/Header'
import { useAuth } from '@/lib/auth'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
export default function LoginPage(): JSX.Element {
const [formData, setFormData] = useState({
email: '',
password: '',
})
const [error, setError] = useState('')
const { login, isLoading, user } = useAuth()
const router = useRouter()
useEffect(() => {
if (user) {
router.push('/files')
}
}, [user, router])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { name, value } = e.target
setFormData((prev) => ({
...prev,
[name]: value,
}))
setError('') // Clear error on input change
}
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault()
setError('')
await login(formData.email, formData.password)
}
return (
<div>
<Header />
<div className="min-h-screen bg-base-200 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full">
<div className="card bg-base-100 shadow">
<div className="card-body">
<div className="text-center mb-6">
<h1 className="text-3xl font-bold">Welcome back!</h1>
<p className="text-base-content/70 mt-2">Sign into your hedgehog account</p>
</div>
{error && (
<div className="alert alert-error mb-4">
<span>{error}</span>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="form-control">
<label className="label">
<span className="label-text font-medium">📧 Email address</span>
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="hedgehog@example.com"
className="input input-bordered w-full input-lg focus:ring-2 focus:ring-primary/20 transition-all"
required
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-medium">🔒 Password</span>
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="Enter your password"
className="input input-bordered w-full input-lg focus:ring-2 focus:ring-primary/20 transition-all"
required
/>
<label className="label">
<Link
href="/forgot-password"
className="label-text-alt link link-hover text-primary"
>
Forgot your password?
</Link>
</label>
</div>
<button
type="submit"
className="btn btn-primary w-full btn-lg hover:scale-105 transition-transform"
disabled={isLoading}
>
{isLoading ? 'Signing in...' : '🚀 Sign in'}
</button>
</form>
<div className="text-center mt-6">
<span className="text-base-content/70">Don't have an account? </span>
<Link href="/signup" className="link link-primary">
Sign up
</Link>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,230 @@
'use client'
import Header from '@/components/Header'
import { posthog } from '@/lib/posthog'
import Link from 'next/link'
import { useEffect } from 'react'
export default function MariusTechTipsPage(): React.JSX.Element {
useEffect(() => {
if (typeof window !== 'undefined') {
posthog.capture('$pageview', {
$current_url: window.location.href,
$host: window.location.host,
$pathname: window.location.pathname,
utm_source: new URLSearchParams(window.location.search).get('utm_source'),
})
}
}, [])
const handleProductAdClick = (adNumber: number, url: string): void => {
posthog.capture('$autocapture', {
$event_type: 'click',
$external_click_url: url,
})
}
return (
<div>
<Header />
<div className="container mx-auto px-4 py-12 max-w-4xl">
{/* Blog Header */}
<div className="text-center mb-12">
<div className="avatar mb-4">
<div className="w-20 rounded-full">
<div className="bg-primary text-white text-2xl w-20 h-20 rounded-full flex items-center justify-center">
👨💻
</div>
</div>
</div>
<h1 className="text-4xl font-bold mb-2">Marius' Tech Tips</h1>
<p className="text-base-content/70 text-lg">Daily tips and tricks for tech-savvy hedgehogs</p>
</div>
{/* Featured Article */}
<article className="card bg-base-100 shadow-md mb-8">
<div className="card-body">
<div className="badge badge-primary mb-4">Featured</div>
<h2 className="card-title text-3xl mb-4">
🔥 5 File Sharing Mistakes That Could Cost You Your Spikes
</h2>
<div className="text-base-content/70 mb-6">
<span>By Marius Hedgehog</span> • <span>December 15, 2024</span> • <span>5 min read</span>
</div>
<div className="prose max-w-none">
<p className="text-lg mb-4">
Hey fellow hedgehogs! 🦔 Today I'm sharing the most common file sharing mistakes I see
in the hedgehog community. These errors can lead to lost files, security breaches, and
even worse - detached spikes!
</p>
<h3 className="text-xl font-bold mb-3">1. Not Using Encrypted File Sharing</h3>
<p className="mb-4">
Many hedgehogs still share files through unencrypted channels. This is like rolling down
a hill without your protective spikes! Always ensure your file sharing platform uses
end-to-end encryption.
</p>
<div className="alert alert-info mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="stroke-current shrink-0 w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<h4 className="font-bold">Pro Tip from Marius:</h4>
<p>
Look for platforms that offer military-grade encryption. Your files should be as
protected as a hedgehog's vulnerable belly!
</p>
</div>
</div>
<h3 className="text-xl font-bold mb-3">2. Sharing Files Without Access Controls</h3>
<p className="mb-4">
Setting proper permissions is crucial. You wouldn't let just any fox into your burrow,
so why give them access to your files? Always review who can view, edit, and share your
documents.
</p>
<h3 className="text-xl font-bold mb-3">3. Ignoring File Size Limits</h3>
<p className="mb-4">
Large files can slow down your entire workflow. Optimize your hedgehog photos and burrow
blueprints before sharing. Consider using compression or breaking large projects into
smaller chunks.
</p>
{/* Product Ads */}
<div className="bg-base-200 p-6 rounded-lg my-8">
<h4 className="font-bold mb-4 text-center">🎯 Sponsored Content</h4>
<div className="grid md:grid-cols-2 gap-4">
<div className="card bg-base-100 shadow-sm">
<div className="card-body">
<h5 className="card-title text-lg">10ft Hedgehog Garden Statue</h5>
<p className="text-sm">Show your hedgehog pride in your garden!</p>
<div className="card-actions justify-end">
<Link
href="https://shop.example.com/products/10ft-hedgehog-statue?utm_source=hedgebox&utm_medium=paid"
className="btn btn-primary btn-sm"
onClick={() =>
handleProductAdClick(
1,
'https://shop.example.com/products/10ft-hedgehog-statue?utm_source=hedgebox&utm_medium=paid'
)
}
>
Shop Now
</Link>
</div>
</div>
</div>
<div className="card bg-base-100 shadow-sm">
<div className="card-body">
<h5 className="card-title text-lg">Hedge-Watching Cruise</h5>
<p className="text-sm">Luxury cruise to observe hedgehogs in the wild!</p>
<div className="card-actions justify-end">
<Link
href="https://travel.example.com/cruise/hedge-watching?utm_source=hedgebox&utm_medium=paid"
className="btn btn-primary btn-sm"
onClick={() =>
handleProductAdClick(
2,
'https://travel.example.com/cruise/hedge-watching?utm_source=hedgebox&utm_medium=paid'
)
}
>
Book Trip
</Link>
</div>
</div>
</div>
</div>
</div>
<h3 className="text-xl font-bold mb-3">4. Not Backing Up Shared Files</h3>
<p className="mb-4">
Always keep copies of important files. Cloud storage is great, but having multiple
backups ensures you'll never lose those precious hedgehog family photos or important
burrow documentation.
</p>
<h3 className="text-xl font-bold mb-3">5. Using Weak Passwords</h3>
<p className="mb-4">
"hedgehog123" is NOT a secure password! Use complex passwords with a mix of letters,
numbers, and symbols. Consider using a password manager to keep track of all your
credentials.
</p>
<div className="alert alert-success mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
className="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<h4 className="font-bold">Speaking of secure file sharing...</h4>
<p>
If you're looking for a platform that addresses all these concerns, check out{' '}
<Link href="/" className="link link-primary font-semibold">
Hedgebox
</Link>
. It's built specifically for hedgehogs who take their file security seriously!
</p>
</div>
</div>
</div>
<div className="card-actions justify-between items-center mt-8">
<div className="flex gap-2">
<div className="badge badge-outline">File Sharing</div>
<div className="badge badge-outline">Security</div>
<div className="badge badge-outline">Tech Tips</div>
</div>
<div className="flex gap-2">
<button className="btn btn-circle btn-outline btn-sm">❤️</button>
<button className="btn btn-circle btn-outline btn-sm">📨</button>
<button className="btn btn-circle btn-outline btn-sm">🔗</button>
</div>
</div>
</div>
</article>
{/* CTA Section */}
<div className="text-center bg-primary/10 p-8 rounded-lg">
<h3 className="text-2xl font-bold mb-4">Ready for Secure File Sharing?</h3>
<p className="text-base-content/70 mb-6">
Don't let these mistakes happen to you. Try Hedgebox today and keep your files as secure as your
spikes!
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/signup" className="btn btn-primary">
Start Free Trial
</Link>
<Link href="/pricing" className="btn btn-outline">
View Pricing
</Link>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,177 @@
'use client'
import Header from '@/components/Header'
import { useAuth } from '@/lib/auth'
import Link from 'next/link'
export default function HomePage(): JSX.Element {
const { user } = useAuth()
return (
<div className="min-h-screen bg-base-100">
<Header />
{/* Hero Section */}
<section className="relative overflow-hidden bg-gradient-subtle">
<div className="absolute inset-0 bg-grid-slate-100/50 [mask-image:linear-gradient(0deg,white,rgba(255,255,255,0.6))] dark:bg-grid-slate-700/25" />
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="pt-20 pb-16 text-center lg:pt-32">
{user ? (
<>
<div className="mb-8 flex items-center justify-center gap-2 animate-fade-in-up">
<div className="inline-flex items-center rounded-full bg-base-200/80 px-3 py-1 text-sm font-medium mb-6 shadow-md">
<span className="text-lg mr-2">👋</span>
Welcome back, hedgehog!
</div>
<div className="avatar mb-6">
<div className="w-8 h-8 rounded-full ring-1 ring-base-300 shadow-md">
<img src={user.avatar} alt={user.name} />
</div>
</div>
</div>
<h1 className="text-4xl font-bold tracking-tight text-base-content sm:text-6xl lg:text-7xl mb-6">
Welcome back, <span className="text-primary">{user.name.split(' ')[0]}</span>
<span className="ml-2">🦔</span>
</h1>
<p className="text-xl text-base-content/70 mb-8 max-w-2xl mx-auto leading-relaxed">
Ready to manage your files with hedgehog-level security and lightning-fast
performance? Your digital spikes are waiting!
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/files"
className="btn btn-primary btn-lg px-8 rounded-xl transition-all"
>
📁 Go to files
</Link>
<Link
href="/account/settings"
className="btn btn-outline btn-lg px-8 rounded-xl transition-all"
>
Account settings
</Link>
</div>
</>
) : (
<>
<div className="mb-8 animate-fade-in-up">
<div className="inline-flex items-center rounded-full bg-base-200/80 px-3 py-1 text-sm font-medium mb-8 shadow-md">
<span className="text-lg mr-2">🍎</span>
Now available in apple flavor!
</div>
</div>
<h1 className="text-4xl font-bold tracking-tight text-base-content sm:text-6xl lg:text-7xl mb-6">
File storage and sharing
<br />
<span className="text-primary">for hedgehogs</span>
</h1>
<p className="text-xl text-base-content/70 mb-8 max-w-3xl mx-auto leading-relaxed">
Store, share, and collaborate on files with rock-solid security 🛡, lightning-fast
performance , and an intuitive interface designed for modern hedgehog families.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<Link
href="/signup"
className="btn btn-primary btn-lg px-8 rounded-xl transition-all"
>
Get started free
</Link>
<Link
href="/pricing"
className="btn btn-outline btn-lg px-8 rounded-xl transition-all"
>
View pricing
</Link>
</div>
</>
)}
</div>
</div>
</section>
{/* Features Section */}
<section className="py-8 bg-base-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight text-base-content sm:text-4xl mb-4">
Why hedgehogs choose Hedgebox
</h2>
<p className="text-lg text-base-content/70 max-w-2xl mx-auto">
Built specifically for the unique needs of hedgehog file management and collaboration.
</p>
</div>
<div className="grid lg:grid-cols-3 gap-8">
<div className="bg-base-100 border border-base-300/50 rounded-2xl p-8 shadow transition-all duration-300">
<div className="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center mb-6">
<span className="text-3xl">🛡</span>
</div>
<h3 className="text-xl font-semibold text-base-content mb-3">Fox-proof</h3>
<p className="text-base-content/70 leading-relaxed">
Our spike-based encryption keeps your files safe from crafty foxes. Share confidently
with granular controls.
</p>
</div>
<div className="bg-base-100 border border-base-300/50 rounded-2xl p-8 shadow transition-all duration-300">
<div className="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center mb-6">
<span className="text-3xl"></span>
</div>
<h3 className="text-xl font-semibold text-base-content mb-3">Instant</h3>
<p className="text-base-content/70 leading-relaxed">
Share and access files faster than you can roll down the hill. Your stuff is always just
a click away.
</p>
</div>
<div className="bg-base-100 border border-base-300/50 rounded-2xl p-8 shadow transition-all duration-300">
<div className="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center mb-6">
<span className="text-3xl">🤝</span>
</div>
<h3 className="text-xl font-semibold text-base-content mb-3">Warm & fuzzy</h3>
<p className="text-base-content/70 leading-relaxed">
Real-time teamwork, notifications, and permissionsso your whole family can work
together.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-16 bg-base-200/50">
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8">
<div className="mb-6">
<span className="text-6xl">🦔</span>
</div>
<h2 className="text-3xl font-bold tracking-tight text-base-content mb-4">
Ready to join the revolution?
</h2>
<p className="text-xl text-base-content/70 mb-8">
Join thousands of hedgehogs already using Hedgebox for their file sharing needs.
</p>
<Link href="/signup" className="btn btn-primary btn-lg px-8 rounded-xl transition-all">
🌟 Start your journey
</Link>
</div>
</section>
{/* Footer */}
<footer className="border-t border-base-300/50 py-12 bg-base-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="w-7 h-7 bg-primary rounded-lg flex items-center justify-center group-hover:scale-105 transition-transform">
<svg className="w-4 h-4 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
<p className="text-base-content/60 text-sm">
© 2024 Hedgebox. All rights reserved. Made with 🌵.
</p>
</div>
</div>
</footer>
</div>
)
}

View File

@@ -0,0 +1,140 @@
'use client'
import Header from '@/components/Header'
import { pricingPlans } from '@/lib/data'
import Link from 'next/link'
export default function PricingPage(): React.JSX.Element {
return (
<div>
<Header />
<div className="bg-gray-50 py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-16">
<h1 className="text-4xl font-bold text-gray-900 mb-4">Choose Your Hedgebox Plan</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Whether you're a solo hedgehog or running a full hedgehog business, we have the perfect plan
for your file sharing needs.
</p>
</div>
{/* Pricing Cards */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{pricingPlans.map((plan, index) => (
<div
key={plan.name}
className={`card bg-base-100 shadow ${
index === 1 ? 'ring-2 ring-primary ring-opacity-50 scale-105' : ''
}`}
>
<div className="card-body">
{index === 1 && (
<div className="badge badge-primary absolute -top-3 left-1/2 transform -translate-x-1/2">
Most Popular
</div>
)}
<div className="text-center">
<h3 className="card-title justify-center text-xl mb-2">{plan.name}</h3>
<div className="mb-4">
<span className="text-4xl font-bold">{plan.price}</span>
<span className="text-base-content/70 ml-1">/{plan.period}</span>
</div>
<div className="badge badge-secondary mb-6">{plan.storage}</div>
</div>
<ul className="space-y-3 mb-8">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center">
<svg
className="w-5 h-5 text-success mr-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span>{feature}</span>
</li>
))}
</ul>
<div className="card-actions justify-center">
<Link
href="/signup"
className={`btn w-full ${index === 1 ? 'btn-primary' : 'btn-outline'}`}
>
{plan.price === '$0' ? 'Start Free' : 'Get Started'}
</Link>
</div>
</div>
</div>
))}
</div>
{/* FAQ Section */}
<div className="mt-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Frequently Asked Questions</h2>
</div>
<div className="max-w-4xl mx-auto">
<div className="grid md:grid-cols-2 gap-8">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Can I upgrade or downgrade anytime?
</h3>
<p className="text-gray-600">
Yes! You can change your plan at any time. Upgrades take effect immediately,
while downgrades take effect at the next billing cycle.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Is my data safe?</h3>
<p className="text-gray-600">
Absolutely. We use enterprise-grade encryption and security measures to keep
your files safe and secure.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Do you offer refunds?</h3>
<p className="text-gray-600">
We offer a 30-day money-back guarantee on all paid plans. No questions asked!
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
What file types are supported?
</h3>
<p className="text-gray-600">
We support all file types! From documents and images to videos and archives - if
hedgehogs need it, we support it.
</p>
</div>
</div>
</div>
</div>
{/* CTA */}
<div className="mt-16 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Still have questions?</h2>
<p className="text-gray-600 mb-6">
Our hedgehog support team is here to help you choose the right plan.
</p>
<Link href="/signup" className="btn btn-primary btn-lg">
Start Your Free Trial
</Link>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,165 @@
'use client'
import Header from '@/components/Header'
import { useAuth } from '@/lib/auth'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
export default function SignupPage(): JSX.Element {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
plan: 'personal/free',
})
const [error, setError] = useState('')
const { signup, isLoading, user } = useAuth()
const router = useRouter()
useEffect(() => {
if (user) {
router.push('/files')
}
}, [user, router])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>): void => {
const { name, value } = e.target
setFormData((prev) => ({
...prev,
[name]: value,
}))
}
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault()
setError('')
await signup(formData.name, formData.email, formData.password, formData.plan)
}
return (
<div>
<Header />
<div className="min-h-screen bg-base-200 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full">
<div className="card bg-base-100 shadow-md">
<div className="card-body">
<div className="text-center mb-6">
<h1 className="text-3xl font-bold">Create your account</h1>
<p className="text-base-content/70 mt-2">
Join thousands of hedgehogs sharing files securely
</p>
</div>
{error && (
<div className="alert alert-error mb-4">
<span>{error}</span>
</div>
)}
<form onSubmit={handleSubmit} method="post" className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text">Full name</span>
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Enter your hedgehog name"
className="input input-bordered w-full"
required
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Email address</span>
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="hedgehog@example.com"
className="input input-bordered w-full"
required
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Password</span>
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="Create a secure password"
className="input input-bordered w-full"
required
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Choose your plan</span>
</label>
<select
name="plan"
value={formData.plan}
onChange={handleInputChange}
className="select select-bordered w-full"
>
<option value="personal/free">Personal Free - $0/month</option>
<option value="personal/pro">Personal Pro - $10/month</option>
<option value="business/standard">Business Standard - $10/user/month</option>
<option value="business/enterprise">
Business Enterprise - $20/user/month
</option>
</select>
</div>
<div className="form-control">
<label className="cursor-pointer label justify-start">
<input type="checkbox" className="checkbox checkbox-primary mr-3" required />
<span className="label-text text-sm">
I agree to the{' '}
<Link href="/terms" className="link link-primary">
Terms of Service
</Link>{' '}
and{' '}
<Link href="/privacy" className="link link-primary">
Privacy Policy
</Link>
</span>
</label>
</div>
<button
type="submit"
className={`btn btn-primary w-full ${isLoading ? 'loading' : ''}`}
disabled={isLoading}
>
{isLoading ? 'Creating Account...' : 'Create Account'}
</button>
</form>
<div className="text-center mt-6">
<span className="text-base-content/70">Already have an account? </span>
<Link href="/login" className="link link-primary">
Sign in
</Link>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,184 @@
'use client'
import { useAuth } from '@/lib/auth'
import Link from 'next/link'
import { useState } from 'react'
export default function Header(): JSX.Element {
const { user, logout } = useAuth()
const [isProfileOpen, setIsProfileOpen] = useState(false)
const handleLogout = (): void => {
logout()
setIsProfileOpen(false)
}
const getAvatarUrl = (): string =>
user?.avatar || `https://api.dicebear.com/7.x/adventurer/svg?seed=${user?.email}&backgroundColor=1e40af`
const navLinks = user
? [{ href: '/files', icon: '📁', text: 'Files' }]
: [
{ href: '/pricing', icon: '📊', text: 'Pricing' },
{ href: '/mariustechtips', icon: '📝', text: 'Blog' },
{ href: '/login', icon: '🔑', text: 'Log in' },
]
const userMenuItems = [
{ href: '/account/settings', icon: '⚙️', text: 'Account Settings' },
{ href: '/account/billing', icon: '💳', text: 'Billing' },
{ href: '/account/team', icon: '👥', text: 'Team' },
]
return (
<div className="navbar bg-base-100/95 backdrop-blur-md shadow-lg border-b border-base-200/50 sticky top-0 z-50">
<div className="navbar-start">
{/* Mobile menu */}
<div className="dropdown">
<div
tabIndex={0}
role="button"
className="btn btn-ghost lg:hidden hover:bg-primary/10 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 bg-base-100 rounded-box w-52 border border-base-200"
>
{navLinks.map((link) => (
<li key={link.href}>
<Link href={link.href} className="hover:bg-primary/10 rounded-lg">
{link.icon} {link.text}
</Link>
</li>
))}
{!user && (
<li className="pt-2">
<Link href="/signup" className="btn btn-primary btn-sm">
Sign up
</Link>
</li>
)}
{user && (
<>
{userMenuItems.map((item) => (
<li key={item.href}>
<Link href={item.href} className="hover:bg-primary/10 rounded-lg">
{item.icon} {item.text}
</Link>
</li>
))}
<li>
<button onClick={handleLogout} className="hover:bg-error/10 text-error rounded-lg">
🚪 Log out
</button>
</li>
</>
)}
</ul>
</div>
{/* Logo */}
<Link href="/" className="btn btn-ghost text-xl hover:bg-primary/10 transition-all duration-300 group">
<div className="w-7 h-7 bg-primary rounded-lg flex items-center justify-center group-hover:scale-105 transition-transform">
<svg className="w-4 h-4 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
<span className="font-bold">Hedgebox</span>
</Link>
</div>
<div className="navbar-end">
<div className="hidden lg:flex items-center space-x-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="btn btn-ghost hover:bg-primary/10 transition-colors"
>
{link.icon} {link.text}
</Link>
))}
{!user && (
<Link href="/signup" className="btn btn-primary hover:scale-105 transition-transform">
Sign up
</Link>
)}
</div>
{user && (
<div className="hidden lg:flex items-center space-x-4">
{/* User Profile Dropdown */}
<div className="dropdown dropdown-end">
<div
tabIndex={0}
role="button"
className="btn btn-ghost hover:bg-primary/10 transition-colors p-2"
onClick={() => setIsProfileOpen(!isProfileOpen)}
>
<div className="flex items-center space-x-2">
<div className="avatar">
<div className="w-8 h-8 rounded-full">
<img src={getAvatarUrl()} alt={user.name} />
</div>
</div>
<span className="hidden xl:block font-medium">{user.name.split(' ')[0]}</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-3 shadow-xl bg-base-100 rounded-box w-64 border border-base-200 mt-2"
>
<li className="mb-2">
<div className="flex items-center space-x-3 p-2 rounded-lg bg-primary/5">
<div className="avatar">
<div className="w-10 h-10 rounded-full">
<img src={getAvatarUrl()} alt={user.name} />
</div>
</div>
<div>
<div className="font-semibold text-sm">{user.name}</div>
<div className="text-xs text-base-content/70">{user.email}</div>
<div className="badge badge-primary badge-xs mt-1">{user.plan}</div>
</div>
</div>
</li>
{userMenuItems.map((item) => (
<li key={item.href}>
<Link href={item.href} className="hover:bg-primary/10 rounded-lg">
{item.icon} {item.text}
</Link>
</li>
))}
<div className="divider my-1" />
<li>
<button onClick={handleLogout} className="hover:bg-error/10 text-error rounded-lg">
🚪 Log out
</button>
</li>
</ul>
</div>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,137 @@
'use client'
import React, { ReactNode, createContext, useContext, useEffect, useState } from 'react'
import { sampleUsers } from './data'
import { posthog } from './posthog'
interface User {
id: string
name: string
email: string
plan: string
avatar?: string
}
interface AuthContextType {
user: User | null
login: (email: string, password: string) => Promise<boolean>
signup: (name: string, email: string, password: string, plan: string) => Promise<boolean>
logout: () => void
isLoading: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
// Load user from localStorage on mount
useEffect(() => {
const savedUser = localStorage.getItem('hedgebox_user')
if (savedUser) {
try {
const userData = JSON.parse(savedUser)
setUser(userData)
// Re-identify user on page load
posthog.identify(userData.id, userData)
} catch {
localStorage.removeItem('hedgebox_user')
}
}
setIsLoading(false)
}, [])
const login = async (email: string /* password is intentionally unused in demo */): Promise<boolean> => {
setIsLoading(true)
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500))
// Find existing user or create new one
let userData = sampleUsers.find((u) => u.email === email)
if (!userData) {
// Create fake user based on email
const name = email.split('@')[0].replace(/[._]/g, ' ')
userData = {
id: `user_${Date.now()}`,
name: name
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '),
email,
plan: 'personal/free',
}
}
const userWithAvatar = {
...userData,
avatar: `https://api.dicebear.com/7.x/adventurer/svg?seed=${encodeURIComponent(userData.email)}&backgroundColor=1e40af`,
}
setUser(userWithAvatar)
localStorage.setItem('hedgebox_user', JSON.stringify(userWithAvatar))
// Track successful login
posthog.capture('logged_in')
posthog.identify(userWithAvatar.id, userWithAvatar)
return true
} catch {
return false
} finally {
setIsLoading(false)
}
}
const signup = async (name: string, email: string, password: string, plan: string): Promise<boolean> => {
setIsLoading(true)
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000))
const userData = {
id: `user_${Date.now()}`,
name,
email,
plan,
avatar: `https://api.dicebear.com/7.x/adventurer/svg?seed=${encodeURIComponent(email)}&backgroundColor=1e40af`,
}
setUser(userData)
localStorage.setItem('hedgebox_user', JSON.stringify(userData))
// Track successful signup
posthog.capture('signed_up', {
from_invite: false,
})
posthog.identify(userData.id, userData)
return true
} catch {
return false
} finally {
setIsLoading(false)
}
}
const logout = (): void => {
setUser(null)
localStorage.removeItem('hedgebox_user')
posthog.capture('logged_out')
posthog.reset()
}
return <AuthContext.Provider value={{ user, login, signup, logout, isLoading }}>{children}</AuthContext.Provider>
}
export function useAuth(): AuthContextType {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}

View File

@@ -0,0 +1,99 @@
import { HedgeboxAccount, HedgeboxFile, HedgeboxUser } from '@/types'
// Sample users
export const sampleUsers: HedgeboxUser[] = [
{
id: 'user1',
name: 'Sonic Hedgehog',
email: 'sonic@hedgebox.net',
plan: 'personal/pro',
},
{
id: 'user2',
name: 'Amy Rose',
email: 'amy@hedgebox.net',
plan: 'business/standard',
},
{
id: 'user3',
name: 'Knuckles Echidna',
email: 'knuckles@hedgebox.net',
plan: 'personal/free',
},
]
// Sample files
export const sampleFiles: HedgeboxFile[] = [
{
id: 'file1',
name: 'ring-collection-2024.jpg',
type: 'image/jpeg',
size: 2400000,
uploadedAt: new Date('2024-01-15'),
},
{
id: 'file2',
name: 'emerald-locations.pdf',
type: 'application/pdf',
size: 850000,
uploadedAt: new Date('2024-01-20'),
sharedLink: 'https://hedgebox.net/files/file2/shared',
},
{
id: 'file3',
name: 'chili-dog-recipe.doc',
type: 'application/msword',
size: 125000,
uploadedAt: new Date('2024-01-25'),
},
{
id: 'file4',
name: 'loop-de-loop-tutorial.mp4',
type: 'video/mp4',
size: 15600000,
uploadedAt: new Date('2024-02-01'),
},
]
// Sample account
export const sampleAccount: HedgeboxAccount = {
id: 'account1',
name: "Sonic's Files",
plan: 'personal/pro',
usedStorage: 19000000, // ~19MB
maxStorage: 1000000000, // 1GB
teamMembers: [sampleUsers[0]],
files: sampleFiles,
}
// Pricing plans data
export const pricingPlans = [
{
name: 'Personal Free',
price: '$0',
period: 'forever',
storage: '10 GB',
features: ['Basic file sharing', 'Email support', '1 user'],
},
{
name: 'Personal Pro',
price: '$10',
period: 'per month',
storage: '1 TB',
features: ['Advanced sharing', 'Priority support', '1 user', 'Version history'],
},
{
name: 'Business Standard',
price: '$10',
period: 'per user/month',
storage: '5 TB',
features: ['Team collaboration', '24/7 support', 'Unlimited users', 'Admin controls'],
},
{
name: 'Business Enterprise',
price: '$20',
period: 'per user/month',
storage: '100 TB',
features: ['Enterprise features', 'Dedicated support', 'SSO', 'Advanced security'],
},
]

View File

@@ -0,0 +1,15 @@
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useAuth } from './auth'
export const useAuthRedirect = (): void => {
const { isLoading, user } = useAuth()
const router = useRouter()
useEffect(() => {
if (!isLoading && !user) {
router.push('/login')
}
}, [isLoading, user, router])
}

View File

@@ -0,0 +1,27 @@
import posthog from 'posthog-js'
export function initPostHog(): void {
if (typeof window !== 'undefined') {
const demoApiToken = process.env.NEXT_PUBLIC_POSTHOG_KEY
if (!demoApiToken) {
console.warn(
'NEXT_PUBLIC_POSTHOG_KEY is not set, skipping PostHog initialization.\n' +
'Run "npm run fetch-key" to automatically fetch the key from the database.'
)
return
}
const localApiHost = process.env.NEXT_PUBLIC_POSTHOG_HOST || 'http://localhost:8010'
posthog.init(demoApiToken, {
api_host: localApiHost,
disable_compression: true,
capture_pageview: false,
autocapture: true,
persistence: 'memory', // Use memory persistence for replay mode to avoid conflicts
opt_out_useragent_filter: true, // We do want capture to work in a bot environment (Playwright)
})
console.info(`PostHog initialized for Hedgebox with host: ${localApiHost}, api token: ${demoApiToken}`)
}
;(window as any).posthog = posthog
}
export { posthog }

View File

@@ -0,0 +1,19 @@
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) {return '0 Bytes'}
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
export const getFileIcon = (type: string): string => {
if (type.startsWith('image/')) {return '🖼️'}
if (type.startsWith('video/')) {return '🎥'}
if (type.startsWith('audio/')) {return '🎵'}
if (type.includes('pdf')) {return '📄'}
if (type.includes('word')) {return '📝'}
if (type.includes('excel') || type.includes('spreadsheet')) {return '📊'}
if (type.includes('powerpoint') || type.includes('presentation')) {return '📈'}
if (type.includes('zip') || type.includes('rar')) {return '🗜️'}
return '📁'
}

View File

@@ -0,0 +1,25 @@
export interface HedgeboxFile {
id: string
name: string
type: string
size: number
uploadedAt: Date
sharedLink?: string
}
export interface HedgeboxUser {
id: string
name: string
email: string
plan: 'personal/free' | 'personal/pro' | 'business/standard' | 'business/enterprise'
}
export interface HedgeboxAccount {
id: string
name: string
plan: 'personal/free' | 'personal/pro' | 'business/standard' | 'business/enterprise'
usedStorage: number
maxStorage: number
teamMembers: HedgeboxUser[]
files: HedgeboxFile[]
}

View File

@@ -0,0 +1,48 @@
/** @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: {
boxShadow: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05)',
DEFAULT:
'0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 4px 8px -1px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.04), 0 8px 16px -4px rgba(0, 0, 0, 0.08)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.05), 0 10px 15px -3px rgba(0, 0, 0, 0.1)',
lg: '0 4px 8px -2px rgba(0, 0, 0, 0.08), 0 8px 16px -4px rgba(0, 0, 0, 0.12), 0 16px 32px -8px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.04), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
},
},
},
plugins: [require('daisyui')],
daisyui: {
themes: [
{
hedgebox: {
primary: '#0f0f23',
'primary-content': '#ffffff',
secondary: '#6b7280',
'secondary-content': '#ffffff',
accent: '#2563eb',
'accent-content': '#ffffff',
neutral: '#1f2937',
'neutral-content': '#f9fafb',
'base-100': '#ffffff',
'base-200': '#f8fafc',
'base-300': '#e2e8f0',
'base-content': '#0f172a',
info: '#0ea5e9',
'info-content': '#ffffff',
success: '#10b981',
'success-content': '#ffffff',
warning: '#f59e0b',
'warning-content': '#ffffff',
error: '#ef4444',
'error-content': '#ffffff',
},
},
],
},
}

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"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"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,6 +1,6 @@
import datetime as dt
from dataclasses import dataclass
from typing import Optional
from typing import TYPE_CHECKING, Optional
from django.db import IntegrityError
@@ -63,6 +63,10 @@ from .taxonomy import (
URL_SIGNUP,
)
if TYPE_CHECKING:
from posthog.models.team import Team
from posthog.models.user import User
@dataclass
class HedgeboxCompany:
@@ -118,9 +122,10 @@ class HedgeboxMatrix(Matrix):
self.new_signup_page_experiment_end = self.now - dt.timedelta(days=2, hours=3, seconds=43)
self.new_signup_page_experiment_start = self.start + (self.new_signup_page_experiment_end - self.start) / 2
def set_project_up(self, team, user):
def set_project_up(self, team: "Team", user: "User"):
super().set_project_up(team, user)
team.autocapture_web_vitals_opt_in = True
team.session_recording_opt_in = True # Also see: the hedgebox-dummy/ app
# Actions
interacted_with_file_action = Action.objects.create(

View File

@@ -167,21 +167,6 @@ class Command(BaseCommand):
gen_issues(team_for_issues)
except exceptions.ValidationError as e:
print(f"Error: {e}")
else:
print(
"\nMaster project reset!\n"
if existing_team_id == 0
else (
f"\nDemo data ready for project {team.name}!\n"
if existing_team_id is not None
else f"\nDemo data ready for {user.email}!\n\n"
"Pre-fill the login form with this link:\n"
f"http://localhost:8000/login?email={user.email}\n"
f"The password is:\n{password}\n\n"
"If running demo mode (DEMO=1), log in instantly with this link:\n"
f"http://localhost:8000/signup?email={user.email}\n"
)
)
if not options.get("skip_materialization"):
print("Materializing common columns...")
self.materialize_common_columns(options["days_past"])
@@ -203,6 +188,20 @@ class Command(BaseCommand):
print("Continuing anyway...")
else:
print("Skipping feature flag sync.")
print(
"\nMaster project reset!\n"
if existing_team_id == 0
else (
f"\nDemo data ready for project {team.name}!\n"
if existing_team_id is not None
else f"\nDemo data ready for {user.email}!\n\n"
"Pre-fill the login form with this link:\n"
f"http://localhost:8000/login?email={user.email}\n"
f"The password is:\n{password}\n\n"
"If running demo mode (DEMO=1), log in instantly with this link:\n"
f"http://localhost:8000/signup?email={user.email}\n"
)
)
else:
print("Dry run - not saving results.")