Initial commit: Full project structure

- Backend: NestJS with Docker
- Frontend: Next.js with Docker
- Nginx configuration for reverse proxy
- PostgreSQL setup
- Docker compose for orchestration
- Development environment configuration
This commit is contained in:
2026-04-23 15:33:11 +03:30
commit 26bd35ae3c
94 changed files with 5627 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
//src/components/ui/Badge.tsx
import React from 'react';
type BadgeVariant = 'success' | 'warning' | 'danger' | 'default';
interface BadgeProps {
children: React.ReactNode;
variant?: BadgeVariant;
className?: string;
}
const variantStyles: Record<BadgeVariant, string> = {
success: 'bg-green-50 text-green-700 border-green-200',
warning: 'bg-yellow-50 text-yellow-700 border-yellow-200',
danger: 'bg-red-50 text-red-700 border-red-200',
default: 'bg-gray-50 text-gray-700 border-gray-200',
};
export function Badge({
children,
variant = 'default',
className,
}: BadgeProps) {
return (
<span
className={`px-2 py-1 text-xs font-medium rounded-lg border inline-block ${variantStyles[variant]} ${className || ''}`}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,67 @@
// src/components/ui/Button.tsx
import React from 'react';
import { Loader2 } from 'lucide-react';
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
fullWidth?: boolean;
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
isLoading = false,
fullWidth = false,
children,
className = '',
disabled,
...props
}) => {
const baseClasses =
'inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200 ' +
'focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed';
const variantClasses: Record<ButtonVariant, string> = {
primary:
'bg-primary text-black hover:opacity-90',
secondary:
'bg-background-secondary text-text-primary hover:bg-background-card',
outline:
'border border-border text-text-primary hover:bg-background-card',
danger:
'bg-red-600 text-white hover:bg-red-700',
ghost:
'text-text-secondary hover:bg-background-card',
};
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
const widthClass = fullWidth ? 'w-full' : '';
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${widthClass} ${className}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading && (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
)}
{children}
</button>
);
};

View File

@@ -0,0 +1,67 @@
// src/components/ui/Input.tsx
import React, { forwardRef } from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
icon?: React.ReactNode;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, icon, className = '', id, ...props }, ref) => {
const inputId =
id || `input-${Math.random().toString(36).slice(2, 9)}`;
return (
<div className="w-full">
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium text-text-secondary mb-1"
>
{label}
</label>
)}
<div className="relative">
{icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center text-text-secondary pointer-events-none">
{icon}
</div>
)}
<input
ref={ref}
id={inputId}
className={`
w-full rounded-lg border
${error ? 'border-red-500' : 'border-border'}
bg-background-secondary text-text-primary
${icon ? 'pl-10' : 'pl-4'} pr-4 py-2
placeholder:text-text-secondary
focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-200
${className}
`}
{...props}
/>
</div>
{error && (
<p className="mt-1 text-sm text-red-500">
{error}
</p>
)}
</div>
);
}
);
Input.displayName = 'Input';

View File

@@ -0,0 +1,38 @@
// src/components/ui/OrganizationCard.tsx
import React from 'react';
import { Building2, Beaker, ChevronRight } from 'lucide-react';
import { Organization } from '@/types';
interface OrganizationCardProps {
organization: Organization;
onSelect: (id: string) => void;
}
export const OrganizationCard: React.FC<OrganizationCardProps> = ({
organization,
onSelect,
}) => {
const Icon = organization.type === 'CLINIC' ? Building2 : Beaker;
const typeText = organization.type === 'CLINIC' ? 'Dental Clinic' : 'Dental Lab';
return (
<button
onClick={() => onSelect(organization.id)}
className="w-full bg-white p-6 rounded-xl shadow-sm border border-gray-200 hover:border-primary-300 hover:shadow-md transition-all text-left flex items-center gap-4 group"
>
<div className="p-3 bg-primary-50 rounded-lg text-primary-600">
<Icon className="h-8 w-8" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">{organization.name}</h3>
<p className="text-sm text-gray-500">{typeText}</p>
{organization.plan && (
<p className="text-xs text-gray-400 mt-1">
Plan: {organization.plan.name} {organization.plan.maxUsers} users
</p>
)}
</div>
<ChevronRight className="h-5 w-5 text-gray-400 group-hover:text-primary-600 transition-colors" />
</button>
);
};

View File

@@ -0,0 +1,57 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
import {
LayoutDashboard,
Users,
Calendar,
UserCog,
FlaskConical,
FileText,
CreditCard
} from 'lucide-react';
const menu = [
{ name: 'Today', path: '/today', icon: LayoutDashboard },
{ name: 'Patients', path: '/patients', icon: Users },
{ name: 'Appointments', path: '/appointments', icon: Calendar },
{ name: 'Staff Management', path: '/staff', icon: UserCog },
{ name: 'Lab Management', path: '/lab', icon: FlaskConical },
{ name: 'Billing', path: '/billing', icon: CreditCard },
{ name: 'Reports', path: '/reports', icon: FileText },
];
export default function Sidebar() {
const pathname = usePathname();
const router = useRouter();
return (
<aside className="w-64 bg-[#071a2f] text-white flex flex-col p-4">
<div className="mb-8">
<h1 className="text-xl font-bold">DyoLink</h1>
</div>
<nav className="flex flex-col gap-2">
{menu.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.path;
return (
<button
key={item.name}
onClick={() => router.push(item.path)}
className={`flex items-center gap-3 p-3 rounded-lg transition ${
isActive
? 'bg-primary-600'
: 'hover:bg-white/10'
}`}
>
<Icon className="w-5 h-5" />
<span>{item.name}</span>
</button>
);
})}
</nav>
</aside>
);
}