frontend-systems 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { Bell, Search, Menu } from 'lucide-react';
4
+
5
+ export function Header() {
6
+ return (
7
+ <header className="flex h-16 items-center justify-between border-b border-dark-800 bg-dark-950 px-6">
8
+ {/* Left side */}
9
+ <div className="flex items-center">
10
+ <button className="rounded-lg p-2 text-dark-400 hover:bg-dark-800 hover:text-white lg:hidden">
11
+ <Menu className="h-5 w-5" />
12
+ </button>
13
+
14
+ {/* Search */}
15
+ <div className="ml-4 hidden lg:block">
16
+ <div className="relative">
17
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-dark-500" />
18
+ <input
19
+ type="text"
20
+ placeholder="Search..."
21
+ className="input w-64 bg-dark-800 pl-10 text-white placeholder:text-dark-500 focus:ring-primary-500"
22
+ />
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ {/* Right side */}
28
+ <div className="flex items-center space-x-4">
29
+ {/* Notifications */}
30
+ <button className="relative rounded-lg p-2 text-dark-400 hover:bg-dark-800 hover:text-white">
31
+ <Bell className="h-5 w-5" />
32
+ <span className="absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary-500 text-[10px] font-medium text-white">
33
+ 3
34
+ </span>
35
+ </button>
36
+
37
+ {/* User avatar (mobile) */}
38
+ <div className="lg:hidden">
39
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary-500/20">
40
+ <span className="text-xs font-medium text-primary-400">AD</span>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </header>
45
+ );
46
+ }
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { clsx } from 'clsx';
6
+ import {
7
+ LayoutDashboard,
8
+ Users,
9
+ ShoppingCart,
10
+ Package,
11
+ Settings,
12
+ BarChart3,
13
+ LogOut,
14
+ } from 'lucide-react';
15
+
16
+ const navigation = [
17
+ { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
18
+ { name: 'Users', href: '/dashboard/users', icon: Users },
19
+ { name: 'Orders', href: '/dashboard/orders', icon: ShoppingCart },
20
+ { name: 'Inventory', href: '/dashboard/inventory', icon: Package },
21
+ { name: 'Analytics', href: '/dashboard/analytics', icon: BarChart3 },
22
+ { name: 'Settings', href: '/dashboard/settings', icon: Settings },
23
+ ];
24
+
25
+ export function Sidebar() {
26
+ const pathname = usePathname();
27
+
28
+ return (
29
+ <div className="flex h-full w-64 flex-col border-r border-dark-800 bg-dark-950">
30
+ {/* Logo */}
31
+ <div className="flex h-16 items-center border-b border-dark-800 px-6">
32
+ <span className="text-xl font-bold text-white">
33
+ ERP<span className="text-primary-500">Dashboard</span>
34
+ </span>
35
+ </div>
36
+
37
+ {/* Navigation */}
38
+ <nav className="flex-1 space-y-1 px-3 py-4">
39
+ {navigation.map((item) => {
40
+ const isActive = pathname === item.href;
41
+ return (
42
+ <Link
43
+ key={item.name}
44
+ href={item.href}
45
+ className={clsx(
46
+ 'group flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
47
+ isActive
48
+ ? 'bg-primary-500/10 text-primary-400'
49
+ : 'text-dark-400 hover:bg-dark-800 hover:text-white'
50
+ )}
51
+ >
52
+ <item.icon
53
+ className={clsx(
54
+ 'mr-3 h-5 w-5 flex-shrink-0 transition-colors',
55
+ isActive ? 'text-primary-400' : 'text-dark-500 group-hover:text-white'
56
+ )}
57
+ />
58
+ {item.name}
59
+ </Link>
60
+ );
61
+ })}
62
+ </nav>
63
+
64
+ {/* User Section */}
65
+ <div className="border-t border-dark-800 p-4">
66
+ <div className="flex items-center">
67
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary-500/20">
68
+ <span className="text-sm font-medium text-primary-400">AD</span>
69
+ </div>
70
+ <div className="ml-3 flex-1">
71
+ <p className="text-sm font-medium text-white">Admin User</p>
72
+ <p className="text-xs text-dark-500">admin@example.com</p>
73
+ </div>
74
+ <button className="rounded-lg p-2 text-dark-500 hover:bg-dark-800 hover:text-white">
75
+ <LogOut className="h-4 w-4" />
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,43 @@
1
+ import { TrendingUp, TrendingDown } from 'lucide-react';
2
+ import { clsx } from 'clsx';
3
+
4
+ interface StatsCardProps {
5
+ title: string;
6
+ value: string;
7
+ change?: {
8
+ value: number;
9
+ trend: 'up' | 'down';
10
+ };
11
+ icon: React.ComponentType<{ className?: string }>;
12
+ }
13
+
14
+ export function StatsCard({ title, value, change, icon: Icon }: StatsCardProps) {
15
+ return (
16
+ <div className="card border-dark-700 p-6">
17
+ <div className="flex items-center justify-between">
18
+ <div className="rounded-lg bg-primary-500/10 p-3">
19
+ <Icon className="h-5 w-5 text-primary-400" />
20
+ </div>
21
+ {change && (
22
+ <div
23
+ className={clsx(
24
+ 'flex items-center text-sm font-medium',
25
+ change.trend === 'up' ? 'text-success-500' : 'text-error-500'
26
+ )}
27
+ >
28
+ {change.trend === 'up' ? (
29
+ <TrendingUp className="mr-1 h-4 w-4" />
30
+ ) : (
31
+ <TrendingDown className="mr-1 h-4 w-4" />
32
+ )}
33
+ {change.value}%
34
+ </div>
35
+ )}
36
+ </div>
37
+ <div className="mt-4">
38
+ <p className="text-sm font-medium text-dark-400">{title}</p>
39
+ <p className="mt-1 text-2xl font-bold text-white">{value}</p>
40
+ </div>
41
+ </div>
42
+ );
43
+ }
package/src/lib/api.ts ADDED
@@ -0,0 +1,249 @@
1
+ /**
2
+ * API Client - Type-safe API wrapper for backend communication.
3
+ */
4
+
5
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
6
+
7
+ interface RequestOptions {
8
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
+ body?: unknown;
10
+ headers?: Record<string, string>;
11
+ }
12
+
13
+ interface ApiError {
14
+ detail: string;
15
+ type?: string;
16
+ }
17
+
18
+ class ApiClient {
19
+ private baseUrl: string;
20
+ private token: string | null = null;
21
+
22
+ constructor(baseUrl: string) {
23
+ this.baseUrl = baseUrl;
24
+ }
25
+
26
+ setToken(token: string | null) {
27
+ this.token = token;
28
+ }
29
+
30
+ private async request<T>(
31
+ endpoint: string,
32
+ options: RequestOptions = {}
33
+ ): Promise<T> {
34
+ const { method = 'GET', body, headers = {} } = options;
35
+
36
+ const requestHeaders: Record<string, string> = {
37
+ 'Content-Type': 'application/json',
38
+ ...headers,
39
+ };
40
+
41
+ if (this.token) {
42
+ requestHeaders['Authorization'] = `Bearer ${this.token}`;
43
+ }
44
+
45
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
46
+ method,
47
+ headers: requestHeaders,
48
+ body: body ? JSON.stringify(body) : undefined,
49
+ });
50
+
51
+ if (!response.ok) {
52
+ const error: ApiError = await response.json().catch(() => ({
53
+ detail: 'An unexpected error occurred',
54
+ }));
55
+ throw new Error(error.detail);
56
+ }
57
+
58
+ // Handle 204 No Content
59
+ if (response.status === 204) {
60
+ return undefined as T;
61
+ }
62
+
63
+ return response.json();
64
+ }
65
+
66
+ // Auth endpoints
67
+ async login(email: string, password: string) {
68
+ const formData = new URLSearchParams();
69
+ formData.append('username', email);
70
+ formData.append('password', password);
71
+
72
+ const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
75
+ body: formData,
76
+ });
77
+
78
+ if (!response.ok) {
79
+ throw new Error('Invalid credentials');
80
+ }
81
+
82
+ return response.json();
83
+ }
84
+
85
+ async getCurrentUser() {
86
+ return this.request<User>('/api/v1/auth/me');
87
+ }
88
+
89
+ // User endpoints
90
+ async getUsers(skip = 0, limit = 50) {
91
+ return this.request<UserListResponse>(`/api/v1/users?skip=${skip}&limit=${limit}`);
92
+ }
93
+
94
+ async getUser(id: string) {
95
+ return this.request<User>(`/api/v1/users/${id}`);
96
+ }
97
+
98
+ async createUser(data: CreateUserRequest) {
99
+ return this.request<User>('/api/v1/users', {
100
+ method: 'POST',
101
+ body: data,
102
+ });
103
+ }
104
+
105
+ async updateUser(id: string, data: UpdateUserRequest) {
106
+ return this.request<User>(`/api/v1/users/${id}`, {
107
+ method: 'PATCH',
108
+ body: data,
109
+ });
110
+ }
111
+
112
+ async deleteUser(id: string) {
113
+ return this.request<void>(`/api/v1/users/${id}`, {
114
+ method: 'DELETE',
115
+ });
116
+ }
117
+
118
+ // Order endpoints
119
+ async getOrders(skip = 0, limit = 50, status?: string) {
120
+ let url = `/api/v1/orders?skip=${skip}&limit=${limit}`;
121
+ if (status) url += `&status=${status}`;
122
+ return this.request<OrderListResponse>(url);
123
+ }
124
+
125
+ async getOrder(id: string) {
126
+ return this.request<Order>(`/api/v1/orders/${id}`);
127
+ }
128
+
129
+ async createOrder(data: CreateOrderRequest) {
130
+ return this.request<Order>('/api/v1/orders', {
131
+ method: 'POST',
132
+ body: data,
133
+ });
134
+ }
135
+
136
+ // Inventory endpoints
137
+ async getInventory(skip = 0, limit = 50, search?: string) {
138
+ let url = `/api/v1/inventory?skip=${skip}&limit=${limit}`;
139
+ if (search) url += `&search=${encodeURIComponent(search)}`;
140
+ return this.request<InventoryListResponse>(url);
141
+ }
142
+
143
+ async getInventoryItem(id: string) {
144
+ return this.request<InventoryItem>(`/api/v1/inventory/${id}`);
145
+ }
146
+
147
+ async getLowStockItems() {
148
+ return this.request<InventoryItem[]>('/api/v1/inventory/low-stock');
149
+ }
150
+
151
+ // Health check
152
+ async healthCheck() {
153
+ return this.request<HealthResponse>('/health');
154
+ }
155
+ }
156
+
157
+ // Types
158
+ export interface User {
159
+ id: string;
160
+ email: string;
161
+ full_name: string;
162
+ role: string;
163
+ is_active: boolean;
164
+ is_verified: boolean;
165
+ created_at: string;
166
+ }
167
+
168
+ export interface UserListResponse {
169
+ items: User[];
170
+ total: number;
171
+ skip: number;
172
+ limit: number;
173
+ }
174
+
175
+ export interface CreateUserRequest {
176
+ email: string;
177
+ password: string;
178
+ full_name: string;
179
+ role?: string;
180
+ }
181
+
182
+ export interface UpdateUserRequest {
183
+ full_name?: string;
184
+ role?: string;
185
+ is_active?: boolean;
186
+ }
187
+
188
+ export interface Order {
189
+ id: string;
190
+ order_number: string;
191
+ customer_name: string;
192
+ customer_email: string | null;
193
+ status: string;
194
+ items: OrderItem[];
195
+ subtotal: number;
196
+ grand_total: number;
197
+ currency: string;
198
+ created_at: string;
199
+ }
200
+
201
+ export interface OrderItem {
202
+ id: string;
203
+ product_name: string;
204
+ quantity: number;
205
+ unit_price: number;
206
+ total: number;
207
+ }
208
+
209
+ export interface OrderListResponse {
210
+ items: Order[];
211
+ total: number;
212
+ skip: number;
213
+ limit: number;
214
+ }
215
+
216
+ export interface CreateOrderRequest {
217
+ customer_name: string;
218
+ customer_email?: string;
219
+ }
220
+
221
+ export interface InventoryItem {
222
+ id: string;
223
+ sku: string;
224
+ name: string;
225
+ description: string | null;
226
+ category: string | null;
227
+ selling_price: number;
228
+ quantity_on_hand: number;
229
+ quantity_available: number;
230
+ needs_reorder: boolean;
231
+ is_active: boolean;
232
+ }
233
+
234
+ export interface InventoryListResponse {
235
+ items: InventoryItem[];
236
+ total: number;
237
+ skip: number;
238
+ limit: number;
239
+ }
240
+
241
+ export interface HealthResponse {
242
+ status: string;
243
+ timestamp: string;
244
+ environment: string;
245
+ version: string;
246
+ }
247
+
248
+ // Singleton instance
249
+ export const apiClient = new ApiClient(API_URL);
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Utility functions for className merging.
3
+ */
4
+
5
+ import { type ClassValue, clsx } from 'clsx';
6
+ import { twMerge } from 'tailwind-merge';
7
+
8
+ /**
9
+ * Merge Tailwind CSS classes with clsx.
10
+ * Handles conflicting classes properly.
11
+ */
12
+ export function cn(...inputs: ClassValue[]) {
13
+ return twMerge(clsx(inputs));
14
+ }
15
+
16
+ /**
17
+ * Format a number as currency.
18
+ */
19
+ export function formatCurrency(
20
+ amount: number,
21
+ currency = 'USD',
22
+ locale = 'en-US'
23
+ ): string {
24
+ return new Intl.NumberFormat(locale, {
25
+ style: 'currency',
26
+ currency,
27
+ }).format(amount);
28
+ }
29
+
30
+ /**
31
+ * Format a date string.
32
+ */
33
+ export function formatDate(
34
+ dateString: string,
35
+ options: Intl.DateTimeFormatOptions = {
36
+ year: 'numeric',
37
+ month: 'short',
38
+ day: 'numeric',
39
+ }
40
+ ): string {
41
+ return new Date(dateString).toLocaleDateString('en-US', options);
42
+ }
43
+
44
+ /**
45
+ * Format a date as relative time (e.g., "2 hours ago").
46
+ */
47
+ export function formatRelativeTime(dateString: string): string {
48
+ const date = new Date(dateString);
49
+ const now = new Date();
50
+ const diffMs = now.getTime() - date.getTime();
51
+ const diffSecs = Math.floor(diffMs / 1000);
52
+ const diffMins = Math.floor(diffSecs / 60);
53
+ const diffHours = Math.floor(diffMins / 60);
54
+ const diffDays = Math.floor(diffHours / 24);
55
+
56
+ if (diffSecs < 60) return 'Just now';
57
+ if (diffMins < 60) return `${diffMins} min ago`;
58
+ if (diffHours < 24) return `${diffHours} hours ago`;
59
+ if (diffDays < 7) return `${diffDays} days ago`;
60
+ return formatDate(dateString);
61
+ }
62
+
63
+ /**
64
+ * Truncate text to a specified length.
65
+ */
66
+ export function truncate(text: string, length: number): string {
67
+ if (text.length <= length) return text;
68
+ return text.slice(0, length) + '...';
69
+ }
70
+
71
+ /**
72
+ * Generate initials from a name.
73
+ */
74
+ export function getInitials(name: string): string {
75
+ return name
76
+ .split(' ')
77
+ .map((n) => n[0])
78
+ .join('')
79
+ .toUpperCase()
80
+ .slice(0, 2);
81
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Common TypeScript type definitions.
3
+ */
4
+
5
+ // User types
6
+ export interface User {
7
+ id: string;
8
+ email: string;
9
+ full_name: string;
10
+ role: 'admin' | 'manager' | 'operator' | 'viewer';
11
+ is_active: boolean;
12
+ is_verified: boolean;
13
+ created_at: string;
14
+ updated_at: string;
15
+ }
16
+
17
+ // Order types
18
+ export type OrderStatus =
19
+ | 'draft'
20
+ | 'pending'
21
+ | 'confirmed'
22
+ | 'processing'
23
+ | 'shipped'
24
+ | 'delivered'
25
+ | 'cancelled'
26
+ | 'refunded';
27
+
28
+ export interface OrderItem {
29
+ id: string;
30
+ product_id: string;
31
+ product_name: string;
32
+ sku: string;
33
+ quantity: number;
34
+ unit_price: number;
35
+ discount_percent: number;
36
+ tax_percent: number;
37
+ total: number;
38
+ }
39
+
40
+ export interface Order {
41
+ id: string;
42
+ order_number: string;
43
+ customer_name: string;
44
+ customer_email: string | null;
45
+ status: OrderStatus;
46
+ items: OrderItem[];
47
+ subtotal: number;
48
+ total_discount: number;
49
+ total_tax: number;
50
+ shipping_cost: number;
51
+ grand_total: number;
52
+ currency: string;
53
+ created_at: string;
54
+ updated_at: string;
55
+ }
56
+
57
+ // Inventory types
58
+ export interface InventoryItem {
59
+ id: string;
60
+ sku: string;
61
+ name: string;
62
+ description: string | null;
63
+ barcode: string | null;
64
+ category: string | null;
65
+ brand: string | null;
66
+ cost_price: number;
67
+ selling_price: number;
68
+ currency: string;
69
+ quantity_on_hand: number;
70
+ quantity_reserved: number;
71
+ quantity_available: number;
72
+ reorder_point: number;
73
+ needs_reorder: boolean;
74
+ is_active: boolean;
75
+ created_at: string;
76
+ }
77
+
78
+ // Pagination
79
+ export interface PaginatedResponse<T> {
80
+ items: T[];
81
+ total: number;
82
+ skip: number;
83
+ limit: number;
84
+ }
85
+
86
+ // API Response types
87
+ export interface ApiError {
88
+ detail: string;
89
+ type?: string;
90
+ }
91
+
92
+ // Auth types
93
+ export interface AuthTokens {
94
+ access_token: string;
95
+ refresh_token: string;
96
+ token_type: string;
97
+ expires_in: number;
98
+ }
99
+
100
+ export interface AuthState {
101
+ user: User | null;
102
+ tokens: AuthTokens | null;
103
+ isAuthenticated: boolean;
104
+ isLoading: boolean;
105
+ }
@@ -0,0 +1,90 @@
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ const config: Config = {
4
+ content: [
5
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8
+ ],
9
+ darkMode: 'class',
10
+ theme: {
11
+ extend: {
12
+ colors: {
13
+ // Brand colors
14
+ primary: {
15
+ 50: '#f0f9ff',
16
+ 100: '#e0f2fe',
17
+ 200: '#bae6fd',
18
+ 300: '#7dd3fc',
19
+ 400: '#38bdf8',
20
+ 500: '#0ea5e9',
21
+ 600: '#0284c7',
22
+ 700: '#0369a1',
23
+ 800: '#075985',
24
+ 900: '#0c4a6e',
25
+ 950: '#082f49',
26
+ },
27
+ // Dark theme background
28
+ dark: {
29
+ 50: '#f9fafb',
30
+ 100: '#f3f4f6',
31
+ 200: '#e5e7eb',
32
+ 300: '#d1d5db',
33
+ 400: '#9ca3af',
34
+ 500: '#6b7280',
35
+ 600: '#4b5563',
36
+ 700: '#374151',
37
+ 800: '#1f2937',
38
+ 900: '#111827',
39
+ 950: '#030712',
40
+ },
41
+ // Success, warning, error
42
+ success: {
43
+ 50: '#f0fdf4',
44
+ 500: '#22c55e',
45
+ 600: '#16a34a',
46
+ },
47
+ warning: {
48
+ 50: '#fffbeb',
49
+ 500: '#f59e0b',
50
+ 600: '#d97706',
51
+ },
52
+ error: {
53
+ 50: '#fef2f2',
54
+ 500: '#ef4444',
55
+ 600: '#dc2626',
56
+ },
57
+ },
58
+ fontFamily: {
59
+ sans: ['Inter', 'system-ui', 'sans-serif'],
60
+ mono: ['JetBrains Mono', 'monospace'],
61
+ },
62
+ spacing: {
63
+ '18': '4.5rem',
64
+ '88': '22rem',
65
+ },
66
+ animation: {
67
+ 'fade-in': 'fadeIn 0.3s ease-in-out',
68
+ 'slide-up': 'slideUp 0.3s ease-out',
69
+ 'slide-down': 'slideDown 0.3s ease-out',
70
+ },
71
+ keyframes: {
72
+ fadeIn: {
73
+ '0%': { opacity: '0' },
74
+ '100%': { opacity: '1' },
75
+ },
76
+ slideUp: {
77
+ '0%': { transform: 'translateY(10px)', opacity: '0' },
78
+ '100%': { transform: 'translateY(0)', opacity: '1' },
79
+ },
80
+ slideDown: {
81
+ '0%': { transform: 'translateY(-10px)', opacity: '0' },
82
+ '100%': { transform: 'translateY(0)', opacity: '1' },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ plugins: [],
88
+ };
89
+
90
+ export default config;