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; 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 *)); @custom-variant dark (&:is(.dark *));
@theme { @theme {

View file

@ -2,7 +2,7 @@ import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm';
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { X } from 'lucide-react'; import ComponentTitle from '@/components/ui/ComponentTitle';
interface InlineFormProps { interface InlineFormProps {
type: 'purchase' | 'milestone' | 'price' | null; type: 'purchase' | 'milestone' | 'price' | null;
@ -28,50 +28,42 @@ export default function InlineForm({
return ( return (
<div <div
className={cn( className={cn(
"bg-black border-4 border-gray-800 rounded-lg", "bg-black p-8",
"shadow-2xl shadow-red-500/20", "transition-all duration-300",
"p-6",
className className
)} )}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="w-full border-4 border-red-500 p-2 bg-black space-y-4 glow-red">
<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>
{/* Form Content */} {/* Form Content */}
<div className="flex justify-center"> <div className="flex justify-center">
{type === 'purchase' ? ( {type === 'purchase' ? (
<AddPurchaseForm <AddPurchaseForm
onSuccess={() => { onSuccess={() => {
if (onPurchaseSuccess) onPurchaseSuccess(); if (onPurchaseSuccess) onPurchaseSuccess();
onClose(); onClose();
}} }}
/> onCancel={onClose}
) : type === 'milestone' ? ( />
<AddMilestoneForm ) : type === 'milestone' ? (
onSuccess={() => { <AddMilestoneForm
if (onMilestoneSuccess) onMilestoneSuccess(); onSuccess={() => {
onClose(); if (onMilestoneSuccess) onMilestoneSuccess();
}} onClose();
/> }}
) : ( onCancel={onClose}
<UpdatePriceForm />
onSuccess={() => { ) : (
if (onPriceSuccess) onPriceSuccess(); <UpdatePriceForm
onClose(); onSuccess={() => {
}} if (onPriceSuccess) onPriceSuccess();
/> onClose();
)} }}
onCancel={onClose}
/>
)}
</div>
</div> </div>
</div> </div>
); );

View file

@ -43,7 +43,7 @@ export default function ProgressBar({
{/* Progress Bar Container */} {/* Progress Bar Container */}
<div className="w-full"> <div className="w-full">
{/* Old-school progress bar with overlaid text */} {/* 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 */} {/* Inner container */}
<div className="relative h-8"> <div className="relative h-8">
{/* Progress fill */} {/* Progress fill */}

View file

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

View file

@ -5,6 +5,7 @@ import InputError from '@/components/InputError';
import { useForm } from '@inertiajs/react'; import { useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react'; import { LoaderCircle } from 'lucide-react';
import { FormEventHandler } from 'react'; import { FormEventHandler } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface MilestoneFormData { interface MilestoneFormData {
target: string; target: string;
@ -14,9 +15,10 @@ interface MilestoneFormData {
interface AddMilestoneFormProps { interface AddMilestoneFormProps {
onSuccess?: () => void; 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>({ const { data, setData, post, processing, errors, reset } = useForm<MilestoneFormData>({
target: '', target: '',
description: '', description: '',
@ -36,11 +38,12 @@ export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) {
}; };
return ( return (
<div className="w-full max-w-md"> <div className="w-full">
<div className="space-y-4"> <div className="space-y-4">
<ComponentTitle>ADD MILESTONE</ComponentTitle>
<form onSubmit={submit} className="space-y-4"> <form onSubmit={submit} className="space-y-4">
<div> <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 <Input
id="target" id="target"
type="number" type="number"
@ -49,32 +52,44 @@ export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) {
placeholder="1500" placeholder="1500"
value={data.target} value={data.target}
onChange={(e) => setData('target', e.target.value)} 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} /> <InputError message={errors.target} />
</div> </div>
<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 <Input
id="description" id="description"
type="text" type="text"
placeholder="First milestone" placeholder="First milestone"
value={data.description} value={data.description}
onChange={(e) => setData('description', e.target.value)} 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} /> <InputError message={errors.description} />
</div> </div>
<Button <div className="flex gap-3 pt-2">
type="submit" <Button
disabled={processing} type="submit"
className="w-full bg-red-600 hover:bg-red-700 text-white border-red-500" 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" />} >
Add Milestone {processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
</Button> [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> </form>
</div> </div>
</div> </div>

View file

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

View file

@ -5,6 +5,7 @@ import InputError from '@/components/InputError';
import { useForm } from '@inertiajs/react'; import { useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react'; import { LoaderCircle } from 'lucide-react';
import { FormEventHandler, useEffect } from 'react'; import { FormEventHandler, useEffect } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface PurchaseFormData { interface PurchaseFormData {
date: string; date: string;
@ -16,9 +17,10 @@ interface PurchaseFormData {
interface AddPurchaseFormProps { interface AddPurchaseFormProps {
onSuccess?: () => void; 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>({ const { data, setData, post, processing, errors, reset } = useForm<PurchaseFormData>({
date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format
shares: '', shares: '',
@ -54,24 +56,25 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
}; };
return ( return (
<div className="w-full max-w-md"> <div className="w-full">
<div className="space-y-4"> <div className="space-y-4">
<ComponentTitle>ADD PURCHASE</ComponentTitle>
<form onSubmit={submit} className="space-y-4"> <form onSubmit={submit} className="space-y-4">
<div> <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 <Input
id="date" id="date"
type="date" type="date"
value={data.date} value={data.date}
onChange={(e) => setData('date', e.target.value)} onChange={(e) => setData('date', e.target.value)}
max={new Date().toISOString().split('T')[0]} 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} /> <InputError message={errors.date} />
</div> </div>
<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 <Input
id="shares" id="shares"
type="number" type="number"
@ -80,13 +83,13 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
placeholder="1.234567" placeholder="1.234567"
value={data.shares} value={data.shares}
onChange={(e) => setData('shares', e.target.value)} 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} /> <InputError message={errors.shares} />
</div> </div>
<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 <Input
id="price_per_share" id="price_per_share"
type="number" type="number"
@ -95,13 +98,13 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
placeholder="123.45" placeholder="123.45"
value={data.price_per_share} value={data.price_per_share}
onChange={(e) => setData('price_per_share', e.target.value)} 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} /> <InputError message={errors.price_per_share} />
</div> </div>
<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 <Input
id="total_cost" id="total_cost"
type="number" type="number"
@ -110,22 +113,34 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) {
placeholder="1234.56" placeholder="1234.56"
value={data.total_cost} value={data.total_cost}
onChange={(e) => setData('total_cost', e.target.value)} 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"> <p className="text-xs text-red-400/60 mt-1 font-mono">
Auto-calculated from shares × price [AUTO-CALC] shares × price
</p> </p>
<InputError message={errors.total_cost} /> <InputError message={errors.total_cost} />
</div> </div>
<Button <div className="flex gap-3 pt-2">
type="submit" <Button
disabled={processing} type="submit"
className="w-full bg-red-600 hover:bg-red-700 text-white border-red-500" 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" />} >
Add Purchase {processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
</Button> [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> </form>
</div> </div>
</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

@ -168,6 +168,7 @@ export default function Dashboard() {
const handleProgressClick = () => { const handleProgressClick = () => {
setShowStatsBox(!showStatsBox); setShowStatsBox(!showStatsBox);
setActiveForm(null)
}; };
return ( return (
@ -186,7 +187,7 @@ export default function Dashboard() {
</div> </div>
{/* Box 2: Progress Bar (toggleable) */} {/* Box 2: Progress Bar (toggleable) */}
<div className="mt-4" style={{ display: showProgressBar ? 'block' : 'none' }}> <div style={{ display: showProgressBar ? 'block' : 'none' }}>
<ProgressBar <ProgressBar
currentShares={purchaseData.total_shares} currentShares={purchaseData.total_shares}
milestones={milestones} milestones={milestones}
@ -196,7 +197,7 @@ export default function Dashboard() {
</div> </div>
{/* Box 3: Stats Box (toggleable) */} {/* Box 3: Stats Box (toggleable) */}
<div className="mt-4" style={{ display: showStatsBox ? 'block' : 'none' }}> <div style={{ display: showStatsBox ? 'block' : 'none' }}>
<StatsBox <StatsBox
stats={statsData} stats={statsData}
milestones={milestones} milestones={milestones}
@ -209,7 +210,7 @@ export default function Dashboard() {
</div> </div>
{/* Box 4: Forms (only when active form is set) */} {/* 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 <InlineForm
type={activeForm} type={activeForm}
onClose={() => setActiveForm(null)} onClose={() => setActiveForm(null)}