Merge pull request 'releases/v0.1' (#18) from releases/v0.1 into main

Reviewed-on: https://codeberg.org/lvl0/incr/pulls/18
This commit is contained in:
Jochen Timmermans 2025-07-13 12:22:59 +02:00
commit 9846905e78
13 changed files with 410 additions and 131 deletions

View file

@ -0,0 +1,44 @@
name: Build and Push Docker Image
on:
push:
tags: [v*]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Codeberg Container Registry
uses: docker/login-action@v3
with:
registry: codeberg.org
username: ${{ secrets.CODEBERG_USERNAME }}
password: ${{ secrets.CODEBERG_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: codeberg.org/lvl0/incr
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

84
docker/Dockerfile Normal file
View file

@ -0,0 +1,84 @@
# Multi-stage build for Laravel + React application
FROM node:20-alpine AS frontend-builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install Node dependencies
RUN npm ci --only=production
# Copy frontend source
COPY resources/ resources/
COPY public/ public/
COPY vite.config.ts ./
COPY tsconfig.json ./
COPY components.json ./
COPY eslint.config.js ./
# Build frontend assets
RUN npm run build
# PHP runtime stage
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libxml2-dev \
zip \
unzip \
oniguruma-dev \
mysql-client
# Install PHP extensions
RUN docker-php-ext-install \
pdo_mysql \
mbstring \
exif \
pcntl \
bcmath \
gd
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application code first
COPY . .
# Install PHP dependencies after copying all files
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Copy built frontend assets from builder stage
COPY --from=frontend-builder /app/public/build/ ./public/build/
# Set proper permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage \
&& chmod -R 755 /var/www/html/bootstrap/cache
# Create a startup script
RUN echo '#!/bin/sh' > /usr/local/bin/start-app \
&& echo 'cp -r /var/www/html/public/. /var/www/html/public_shared/ 2>/dev/null || true' >> /usr/local/bin/start-app \
&& echo 'php artisan config:cache' >> /usr/local/bin/start-app \
&& echo 'php artisan route:cache' >> /usr/local/bin/start-app \
&& echo 'php artisan view:cache' >> /usr/local/bin/start-app \
&& echo 'php artisan migrate --force' >> /usr/local/bin/start-app \
&& echo 'php-fpm' >> /usr/local/bin/start-app \
&& chmod +x /usr/local/bin/start-app
# Expose port 9000 for PHP-FPM
EXPOSE 9000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD php artisan --version || exit 1
# Start the application
CMD ["/usr/local/bin/start-app"]

65
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,65 @@
version: '3.8'
services:
app:
image: codeberg.org/lvl0/incr:v0.1.0-alpha-1
# build:
# context: ../
# dockerfile: docker/Dockerfile
container_name: incr-app
restart: unless-stopped
working_dir: /var/www/html
environment:
- APP_ENV=production
- APP_DEBUG=false
- DB_CONNECTION=mysql
- DB_HOST=db
- DB_PORT=3306
- DB_DATABASE=incr
- DB_USERNAME=incr_user
- DB_PASSWORD=incr_password
volumes:
- ../storage:/var/www/html/storage
- ../public:/var/www/html/public
depends_on:
- db
networks:
- incr-network
db:
image: mysql:8.0
container_name: incr-db
restart: unless-stopped
environment:
- MYSQL_DATABASE=incr
- MYSQL_USER=incr_user
- MYSQL_PASSWORD=incr_password
- MYSQL_ROOT_PASSWORD=root_password
volumes:
- db_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- incr-network
nginx:
image: nginx:alpine
container_name: incr-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ../public:/var/www/html/public:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
networks:
- incr-network
networks:
incr-network:
driver: bridge
volumes:
db_data:
driver: local

26
docker/nginx.conf Normal file
View file

@ -0,0 +1,26 @@
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View file

@ -16,6 +16,15 @@ .font-digital {
font-family: '7Segment', monospace;
}
.glow-red {
box-shadow: 0 0 20px rgba(239, 68, 68, 0.4);
transition: box-shadow 300ms ease;
}
.glow-red:hover {
box-shadow: 0 0 25px rgba(239, 68, 68, 0.6);
}
@custom-variant dark (&:is(.dark *));
@theme {

View file

@ -2,7 +2,7 @@ import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm';
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
import { cn } from '@/lib/utils';
import { X } from 'lucide-react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface InlineFormProps {
type: 'purchase' | 'milestone' | 'price' | null;
@ -13,66 +13,58 @@ interface InlineFormProps {
className?: string;
}
export default function InlineForm({
type,
onClose,
export default function InlineForm({
type,
onClose,
onPurchaseSuccess,
onMilestoneSuccess,
onPriceSuccess,
className
className
}: InlineFormProps) {
if (!type) return null;
const title = type === 'purchase' ? 'ADD PURCHASE' : type === 'milestone' ? 'ADD MILESTONE' : 'UPDATE PRICE';
return (
<div
<div
className={cn(
"bg-black border-4 border-gray-800 rounded-lg",
"shadow-2xl shadow-red-500/20",
"p-6",
"bg-black p-8",
"transition-all duration-300",
className
)}
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-red-500 font-mono tracking-wide text-lg">
{title}
</h2>
<button
onClick={onClose}
className="text-red-400 hover:text-red-300 transition-colors p-1"
aria-label="Close form"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="w-full border-4 border-red-500 p-2 bg-black space-y-4 glow-red">
{/* Form Content */}
<div className="flex justify-center">
{type === 'purchase' ? (
<AddPurchaseForm
onSuccess={() => {
if (onPurchaseSuccess) onPurchaseSuccess();
onClose();
}}
/>
) : type === 'milestone' ? (
<AddMilestoneForm
onSuccess={() => {
if (onMilestoneSuccess) onMilestoneSuccess();
onClose();
}}
/>
) : (
<UpdatePriceForm
onSuccess={() => {
if (onPriceSuccess) onPriceSuccess();
onClose();
}}
/>
)}
{/* Form Content */}
<div className="flex justify-center">
{type === 'purchase' ? (
<AddPurchaseForm
onSuccess={() => {
if (onPurchaseSuccess) onPurchaseSuccess();
onClose();
}}
onCancel={onClose}
/>
) : type === 'milestone' ? (
<AddMilestoneForm
onSuccess={() => {
if (onMilestoneSuccess) onMilestoneSuccess();
onClose();
}}
onCancel={onClose}
/>
) : (
<UpdatePriceForm
onSuccess={() => {
if (onPriceSuccess) onPriceSuccess();
onClose();
}}
onCancel={onClose}
/>
)}
</div>
</div>
</div>
);
}
}

View file

@ -43,7 +43,7 @@ export default function ProgressBar({
{/* Progress Bar Container */}
<div className="w-full">
{/* Old-school progress bar with overlaid text */}
<div className="w-full border-4 border-red-500 p-2 bg-black relative overflow-hidden">
<div className="w-full border-4 border-red-500 p-2 bg-black relative overflow-hidden glow-red">
{/* Inner container */}
<div className="relative h-8">
{/* Progress fill */}

View file

@ -1,6 +1,7 @@
import { cn } from '@/lib/utils';
import { Plus, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface Milestone {
target: number;
@ -69,12 +70,11 @@ export default function StatsBox({
className
)}
>
<div className="w-full border-4 border-red-500 p-2 bg-black space-y-4">
<div className="w-full border-4 border-red-500 p-2 bg-black space-y-4 glow-red">
{/* STATS Title and Current Price */}
<div className="flex justify-between items-center mb-6 relative">
<h2 className="text-red-500 text-lg font-mono font-bold tracking-wider">
STATS
</h2>
<ComponentTitle>Stats</ComponentTitle>
<div className="flex items-center space-x-2 relative">
{stats.currentPrice && (
<div className="text-red-500 text-sm font-mono tracking-wider">
@ -185,8 +185,8 @@ export default function StatsBox({
key={index}
className={cn(
isSelectedMilestone
? "text-red-500 font-bold"
: "bg-red-500 text-black"
? "bg-red-500 text-black"
: "text-red-500 font-bold"
)}
>
<td className="py-1 pr-4">

View file

@ -5,6 +5,7 @@ import InputError from '@/components/InputError';
import { useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react';
import { FormEventHandler } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface MilestoneFormData {
target: string;
@ -14,9 +15,10 @@ interface MilestoneFormData {
interface AddMilestoneFormProps {
onSuccess?: () => void;
onCancel?: () => void;
}
export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) {
export default function AddMilestoneForm({ onSuccess, onCancel }: AddMilestoneFormProps) {
const { data, setData, post, processing, errors, reset } = useForm<MilestoneFormData>({
target: '',
description: '',
@ -36,11 +38,12 @@ export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) {
};
return (
<div className="w-full max-w-md">
<div className="w-full">
<div className="space-y-4">
<ComponentTitle>ADD MILESTONE</ComponentTitle>
<form onSubmit={submit} className="space-y-4">
<div>
<Label htmlFor="target" className="text-red-400">Target Number</Label>
<Label htmlFor="target" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Target Number</Label>
<Input
id="target"
type="number"
@ -49,34 +52,46 @@ export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) {
placeholder="1500"
value={data.target}
onChange={(e) => setData('target', e.target.value)}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30"
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<InputError message={errors.target} />
</div>
<div>
<Label htmlFor="description" className="text-red-400">Description</Label>
<Label htmlFor="description" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Description</Label>
<Input
id="description"
type="text"
placeholder="First milestone"
value={data.description}
onChange={(e) => setData('description', e.target.value)}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30"
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<InputError message={errors.description} />
</div>
<Button
type="submit"
disabled={processing}
className="w-full bg-red-600 hover:bg-red-700 text-white border-red-500"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
Add Milestone
</Button>
<div className="flex gap-3 pt-2">
<Button
type="submit"
disabled={processing}
className="flex-1 bg-red-500 hover:bg-red-500 text-black font-mono text-sm font-bold border-red-500 rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
[EXECUTE]
</Button>
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 bg-black border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300 font-mono text-sm font-bold rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
[ABORT]
</Button>
)}
</div>
</form>
</div>
</div>
);
}
}

View file

@ -1,11 +1,11 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import InputError from '@/components/InputError';
import { useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react';
import { FormEventHandler } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface PriceUpdateFormData {
date: string;
@ -17,9 +17,10 @@ interface UpdatePriceFormProps {
currentPrice?: number;
className?: string;
onSuccess?: () => void;
onCancel?: () => void;
}
export default function UpdatePriceForm({ currentPrice, className, onSuccess }: UpdatePriceFormProps) {
export default function UpdatePriceForm({ currentPrice, className, onSuccess, onCancel }: UpdatePriceFormProps) {
const { data, setData, post, processing, errors } = useForm<PriceUpdateFormData>({
date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format
price: currentPrice?.toString() || '',
@ -38,31 +39,30 @@ export default function UpdatePriceForm({ currentPrice, className, onSuccess }:
};
return (
<Card className={className}>
<CardHeader>
<CardTitle>Update Asset Price</CardTitle>
<div className="w-full">
<div className="space-y-4">
<ComponentTitle>UPDATE PRICE</ComponentTitle>
{currentPrice && (
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Current price: {currentPrice.toFixed(4)}
<p className="text-sm text-red-400/60 font-mono">
[CURRENT] {currentPrice.toFixed(4)}
</p>
)}
</CardHeader>
<CardContent>
<form onSubmit={submit} className="space-y-4">
<div>
<Label htmlFor="date">Price Date</Label>
<Label htmlFor="date" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Price Date</Label>
<Input
id="date"
type="date"
value={data.date}
onChange={(e) => setData('date', e.target.value)}
max={new Date().toISOString().split('T')[0]}
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none transition-all glow-red"
/>
<InputError message={errors.date} />
</div>
<div>
<Label htmlFor="price">Asset Price ()</Label>
<Label htmlFor="price" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Asset Price ()</Label>
<Input
id="price"
type="number"
@ -71,23 +71,36 @@ export default function UpdatePriceForm({ currentPrice, className, onSuccess }:
placeholder="123.4567"
value={data.price}
onChange={(e) => setData('price', e.target.value)}
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all"
/>
<p className="text-xs text-neutral-500 mt-1">
Price per unit/share of the asset
<p className="text-xs text-red-400/60 mt-1 font-mono">
[UNIT] price per share
</p>
<InputError message={errors.price} />
</div>
<Button
type="submit"
disabled={processing}
className="w-full"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
Update Price
</Button>
<div className="flex gap-3 pt-2">
<Button
type="submit"
disabled={processing}
className="flex-1 bg-red-500 hover:bg-red-500 text-black font-mono text-sm font-bold border-red-500 rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
[EXECUTE]
</Button>
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 bg-black border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300 font-mono text-sm font-bold rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
[ABORT]
</Button>
)}
</div>
</form>
</CardContent>
</Card>
</div>
</div>
);
}

View file

@ -5,6 +5,7 @@ import InputError from '@/components/InputError';
import { useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react';
import { FormEventHandler, useEffect } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface PurchaseFormData {
date: string;
@ -16,9 +17,10 @@ interface PurchaseFormData {
interface AddPurchaseFormProps {
onSuccess?: () => void;
onCancel?: () => void;
}
export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
export default function AddPurchaseForm({ onSuccess, onCancel }: AddPurchaseFormProps) {
const { data, setData, post, processing, errors, reset } = useForm<PurchaseFormData>({
date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format
shares: '',
@ -31,7 +33,7 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
if (data.shares && data.price_per_share) {
const shares = parseFloat(data.shares);
const pricePerShare = parseFloat(data.price_per_share);
if (!isNaN(shares) && !isNaN(pricePerShare)) {
const totalCost = (shares * pricePerShare).toFixed(2);
setData('total_cost', totalCost);
@ -54,24 +56,25 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
};
return (
<div className="w-full max-w-md">
<div className="w-full">
<div className="space-y-4">
<ComponentTitle>ADD PURCHASE</ComponentTitle>
<form onSubmit={submit} className="space-y-4">
<div>
<Label htmlFor="date" className="text-red-400">Purchase Date</Label>
<Label htmlFor="date" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Purchase Date</Label>
<Input
id="date"
type="date"
value={data.date}
onChange={(e) => setData('date', e.target.value)}
max={new Date().toISOString().split('T')[0]}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400"
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none transition-all glow-red"
/>
<InputError message={errors.date} />
</div>
<div>
<Label htmlFor="shares" className="text-red-400">Number of Shares</Label>
<Label htmlFor="shares" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Number of Shares</Label>
<Input
id="shares"
type="number"
@ -80,13 +83,13 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
placeholder="1.234567"
value={data.shares}
onChange={(e) => setData('shares', e.target.value)}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30"
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<InputError message={errors.shares} />
</div>
<div>
<Label htmlFor="price_per_share" className="text-red-400">Price per Share ()</Label>
<Label htmlFor="price_per_share" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Price per Share ()</Label>
<Input
id="price_per_share"
type="number"
@ -95,13 +98,13 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
placeholder="123.45"
value={data.price_per_share}
onChange={(e) => setData('price_per_share', e.target.value)}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30"
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<InputError message={errors.price_per_share} />
</div>
<div>
<Label htmlFor="total_cost" className="text-red-400">Total Cost ()</Label>
<Label htmlFor="total_cost" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Total Cost ()</Label>
<Input
id="total_cost"
type="number"
@ -110,24 +113,36 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
placeholder="1234.56"
value={data.total_cost}
onChange={(e) => setData('total_cost', e.target.value)}
className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30"
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<p className="text-xs text-red-400/60 mt-1">
Auto-calculated from shares × price
<p className="text-xs text-red-400/60 mt-1 font-mono">
[AUTO-CALC] shares × price
</p>
<InputError message={errors.total_cost} />
</div>
<Button
type="submit"
disabled={processing}
className="w-full bg-red-600 hover:bg-red-700 text-white border-red-500"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
Add Purchase
</Button>
<div className="flex gap-3 pt-2">
<Button
type="submit"
disabled={processing}
className="flex-1 bg-red-500 hover:bg-red-500 text-black font-mono text-sm font-bold border-red-500 rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
[EXECUTE]
</Button>
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 bg-black border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300 font-mono text-sm font-bold rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
[ABORT]
</Button>
)}
</div>
</form>
</div>
</div>
);
}
}

View file

@ -0,0 +1,15 @@
import { FC, ReactNode } from 'react';
interface ComponentTitleProps {
children: ReactNode;
}
const ComponentTitle: FC<ComponentTitleProps> = ({ children }) => {
return (
<h2 className="text-red-500 text-lg font-mono font-bold tracking-wider uppercase">
{ children }
</h2>
)
}
export default ComponentTitle

View file

@ -27,11 +27,11 @@ export default function Dashboard() {
total_investment: 0,
average_cost_per_share: 0,
});
const [priceData, setPriceData] = useState<CurrentPrice>({
current_price: null,
});
const [milestones, setMilestones] = useState<Milestone[]>([]);
const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0);
const [showProgressBar, setShowProgressBar] = useState(false);
@ -121,14 +121,14 @@ export default function Dashboard() {
// Calculate portfolio stats
const currentValue = priceData.current_price
? purchaseData.total_shares * priceData.current_price
const currentValue = priceData.current_price
? purchaseData.total_shares * priceData.current_price
: undefined;
const profitLoss = currentValue
? currentValue - purchaseData.total_investment
const profitLoss = currentValue
? currentValue - purchaseData.total_investment
: undefined;
const profitLossPercentage = profitLoss && purchaseData.total_investment > 0
? (profitLoss / purchaseData.total_investment) * 100
: undefined;
@ -168,36 +168,37 @@ export default function Dashboard() {
const handleProgressClick = () => {
setShowStatsBox(!showStatsBox);
setActiveForm(null)
};
return (
<>
<Head title="VWCE Tracker" />
{/* Stacked Layout */}
<div className="min-h-screen bg-black">
<div className="w-full max-w-4xl mx-auto px-4">
{/* Box 1: LED Number Display - Fixed position from top */}
<div className="pt-32">
<LedDisplay
<LedDisplay
value={purchaseData.total_shares}
onClick={handleLedClick}
/>
</div>
{/* Box 2: Progress Bar (toggleable) */}
<div className="mt-4" style={{ display: showProgressBar ? 'block' : 'none' }}>
<ProgressBar
<div style={{ display: showProgressBar ? 'block' : 'none' }}>
<ProgressBar
currentShares={purchaseData.total_shares}
milestones={milestones}
selectedMilestoneIndex={selectedMilestoneIndex}
onClick={handleProgressClick}
/>
</div>
{/* Box 3: Stats Box (toggleable) */}
<div className="mt-4" style={{ display: showStatsBox ? 'block' : 'none' }}>
<StatsBox
<div style={{ display: showStatsBox ? 'block' : 'none' }}>
<StatsBox
stats={statsData}
milestones={milestones}
selectedMilestoneIndex={selectedMilestoneIndex}
@ -207,9 +208,9 @@ export default function Dashboard() {
onUpdatePrice={() => setActiveForm('price')}
/>
</div>
{/* Box 4: Forms (only when active form is set) */}
<div className="mt-4" style={{ display: activeForm ? 'block' : 'none' }}>
<div style={{ display: activeForm && showProgressBar && showStatsBox ? 'block' : 'none' }}>
<InlineForm
type={activeForm}
onClose={() => setActiveForm(null)}