Initial commit: MOPC platform with Docker deployment setup

Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth.
Includes production Dockerfile (multi-stage, port 7600), docker-compose
with registry-based image pull, Gitea Actions CI workflow, nginx config
for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 13:41:32 +01:00
commit a606292aaa
290 changed files with 70691 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { UseFormReturn, useFieldArray } from 'react-hook-form'
import { Plus, Trash2, Users } from 'lucide-react'
import { WizardStepContent } from '@/components/forms/form-wizard'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import type { ApplicationFormData } from '@/server/routers/application'
import { TeamMemberRole } from '@prisma/client'
const roleOptions: { value: TeamMemberRole; label: string }[] = [
{ value: 'MEMBER', label: 'Team Member' },
{ value: 'ADVISOR', label: 'Advisor' },
]
interface StepTeamProps {
form: UseFormReturn<ApplicationFormData>
}
export function StepTeam({ form }: StepTeamProps) {
const { control, register, formState: { errors } } = form
const { fields, append, remove } = useFieldArray({
control,
name: 'teamMembers',
})
const addMember = () => {
append({ name: '', email: '', role: 'MEMBER', title: '' })
}
return (
<WizardStepContent
title="Your team members"
description="Add the other members of your team. They will receive an invitation to create their account."
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mx-auto max-w-lg space-y-6"
>
{fields.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 py-12 text-center">
<Users className="mb-4 h-12 w-12 text-muted-foreground/50" />
<p className="text-muted-foreground">
No team members added yet.
</p>
<p className="mt-1 text-sm text-muted-foreground">
You can add team members here, or skip this step if you&apos;re applying solo.
</p>
<Button
type="button"
variant="outline"
onClick={addMember}
className="mt-4"
>
<Plus className="mr-2 h-4 w-4" />
Add Team Member
</Button>
</div>
) : (
<div className="space-y-4">
<AnimatePresence mode="popLayout">
{fields.map((field, index) => (
<motion.div
key={field.id}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="rounded-lg border bg-card p-4"
>
<div className="flex items-start justify-between mb-4">
<h4 className="font-medium text-sm text-muted-foreground">
Team Member {index + 1}
</h4>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
className="h-8 w-8 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2">
{/* Name */}
<div className="space-y-2">
<Label htmlFor={`teamMembers.${index}.name`}>
Full Name <span className="text-destructive">*</span>
</Label>
<Input
id={`teamMembers.${index}.name`}
placeholder="Jane Doe"
{...register(`teamMembers.${index}.name`)}
/>
{errors.teamMembers?.[index]?.name && (
<p className="text-sm text-destructive">
{errors.teamMembers[index]?.name?.message}
</p>
)}
</div>
{/* Email */}
<div className="space-y-2">
<Label htmlFor={`teamMembers.${index}.email`}>
Email <span className="text-destructive">*</span>
</Label>
<Input
id={`teamMembers.${index}.email`}
type="email"
placeholder="jane@example.com"
{...register(`teamMembers.${index}.email`)}
/>
{errors.teamMembers?.[index]?.email && (
<p className="text-sm text-destructive">
{errors.teamMembers[index]?.email?.message}
</p>
)}
</div>
{/* Role */}
<div className="space-y-2">
<Label>Role</Label>
<Select
value={form.watch(`teamMembers.${index}.role`)}
onValueChange={(value) =>
form.setValue(`teamMembers.${index}.role`, value as TeamMemberRole)
}
>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
{roleOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Title/Position */}
<div className="space-y-2">
<Label htmlFor={`teamMembers.${index}.title`}>
Title/Position <span className="text-muted-foreground text-xs">(optional)</span>
</Label>
<Input
id={`teamMembers.${index}.title`}
placeholder="CTO, Designer, etc."
{...register(`teamMembers.${index}.title`)}
/>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
<Button
type="button"
variant="outline"
onClick={addMember}
className="w-full"
>
<Plus className="mr-2 h-4 w-4" />
Add Another Team Member
</Button>
</div>
)}
</motion.div>
</WizardStepContent>
)
}