mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
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:
@@ -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
3
hedgebox-dummy/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.next
|
||||
.env.local
|
||||
node_modules
|
||||
64
hedgebox-dummy/README.md
Normal file
64
hedgebox-dummy/README.md
Normal 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
5
hedgebox-dummy/next-env.d.ts
vendored
Normal 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.
|
||||
34
hedgebox-dummy/package.json
Normal file
34
hedgebox-dummy/package.json
Normal 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
3940
hedgebox-dummy/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
0
hedgebox-dummy/pnpm-workspace.yaml
Normal file
0
hedgebox-dummy/pnpm-workspace.yaml
Normal file
6
hedgebox-dummy/postcss.config.js
Normal file
6
hedgebox-dummy/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
64
hedgebox-dummy/scripts/fetch-posthog-key.js
Executable file
64
hedgebox-dummy/scripts/fetch-posthog-key.js
Executable 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()
|
||||
322
hedgebox-dummy/src/app/files/[id]/page.tsx
Normal file
322
hedgebox-dummy/src/app/files/[id]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
397
hedgebox-dummy/src/app/files/page.tsx
Normal file
397
hedgebox-dummy/src/app/files/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
116
hedgebox-dummy/src/app/globals.css
Normal file
116
hedgebox-dummy/src/app/globals.css
Normal 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%);
|
||||
}
|
||||
23
hedgebox-dummy/src/app/layout.tsx
Normal file
23
hedgebox-dummy/src/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
120
hedgebox-dummy/src/app/login/page.tsx
Normal file
120
hedgebox-dummy/src/app/login/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
230
hedgebox-dummy/src/app/mariustechtips/page.tsx
Normal file
230
hedgebox-dummy/src/app/mariustechtips/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
177
hedgebox-dummy/src/app/page.tsx
Normal file
177
hedgebox-dummy/src/app/page.tsx
Normal 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 permissions—so 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>
|
||||
)
|
||||
}
|
||||
140
hedgebox-dummy/src/app/pricing/page.tsx
Normal file
140
hedgebox-dummy/src/app/pricing/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
165
hedgebox-dummy/src/app/signup/page.tsx
Normal file
165
hedgebox-dummy/src/app/signup/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
184
hedgebox-dummy/src/components/Header.tsx
Normal file
184
hedgebox-dummy/src/components/Header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
137
hedgebox-dummy/src/lib/auth.tsx
Normal file
137
hedgebox-dummy/src/lib/auth.tsx
Normal 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
|
||||
}
|
||||
99
hedgebox-dummy/src/lib/data.ts
Normal file
99
hedgebox-dummy/src/lib/data.ts
Normal 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'],
|
||||
},
|
||||
]
|
||||
15
hedgebox-dummy/src/lib/hooks.ts
Normal file
15
hedgebox-dummy/src/lib/hooks.ts
Normal 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])
|
||||
}
|
||||
27
hedgebox-dummy/src/lib/posthog.ts
Normal file
27
hedgebox-dummy/src/lib/posthog.ts
Normal 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 }
|
||||
19
hedgebox-dummy/src/lib/utils.ts
Normal file
19
hedgebox-dummy/src/lib/utils.ts
Normal 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 '📁'
|
||||
}
|
||||
25
hedgebox-dummy/src/types/index.ts
Normal file
25
hedgebox-dummy/src/types/index.ts
Normal 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[]
|
||||
}
|
||||
48
hedgebox-dummy/tailwind.config.js
Normal file
48
hedgebox-dummy/tailwind.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
28
hedgebox-dummy/tsconfig.json
Normal file
28
hedgebox-dummy/tsconfig.json
Normal 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"]
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user