ca-grow-ops-manager/frontend
fullsizemalt 1f7f722238 Fix nginx DNS resolver for Docker upstream
Use Docker internal DNS resolver and variable for backend upstream
to prevent nginx from failing to resolve hostname during config reload.
2026-01-09 01:44:39 -08:00
..
android fix(android): Remove fitsSystemWindows to fix content truncation 2026-01-07 21:06:57 -08:00
public feat(ops): Add static download.html bypass 2026-01-07 13:55:51 -08:00
src Add tinypdf-plus integration with PDF generation and branding 2026-01-08 12:07:35 -08:00
capacitor.config.ts fix(android): Prevent status bar from overlapping content 2026-01-07 20:05:28 -08:00
components.json feat: Setup React Router, Tailwind, and shadcn/ui components 2025-12-09 01:19:33 -08:00
Dockerfile fix: add --legacy-peer-deps to Dockerfile for React Three Fiber compatibility 2025-12-17 01:59:53 -08:00
index.html Add data-theme attribute to HTML for initial styling 2026-01-08 21:02:39 -08:00
nginx.conf Fix nginx DNS resolver for Docker upstream 2026-01-09 01:44:39 -08:00
package-lock.json fix(android): Remove deprecated @capacitor/http (use built-in CapacitorHttp) 2026-01-07 19:45:58 -08:00
package.json fix(build): Skip tsc in build to bypass TS2347 errors for CapacitorHttp 2026-01-07 20:51:26 -08:00
postcss.config.js fix: Revert to Tailwind v3 for Shadcn compatibility 2025-12-09 11:30:16 -08:00
README.md
tailwind.config.js refactor(ui): theme harmonization and semantic tokens 2026-01-01 19:05:26 -08:00
tsconfig.json fix(types): Temporarily disable strict mode for CapacitorHttp 2026-01-07 20:46:48 -08:00
tsconfig.node.json fix: Add missing project files for backend/frontend build 2025-12-09 00:23:28 -08:00
vite.config.ts feat(android): Add Capacitor for Android APK build 2026-01-06 21:56:28 -08:00
vitest.config.ts feat: Full Spec Kit compliance implementation 2025-12-11 09:53:32 -08:00

CA Grow Ops Manager — Frontend

Version: 0.1.0
Stack: TypeScript, React, Vite/Next.js, Radix UI/shadcn


Overview

The frontend is a mobile-first, accessible web application optimized for low-friction daily use in cultivation facilities. It provides intuitive interfaces for tasks, batches, labor tracking, compliance, and team communication.


Tech Stack

  • Language: TypeScript 5.x
  • Framework: React 18.x
  • Build Tool: Vite or Next.js (to be decided during implementation)
  • Component Library: Radix UI or shadcn-style accessible primitives
  • Styling: Tailwind CSS or modern CSS-in-JS
  • State Management: React Context + hooks (Zustand or Redux Toolkit for complex state)
  • Routing: React Router (Vite) or Next.js App Router
  • Forms: React Hook Form + Zod validation
  • API Client: Axios or Fetch with TypeScript types
  • Testing: Vitest + React Testing Library

Project Structure

Vite Option

frontend/
├── src/
│   ├── app/                  # App shell
│   │   ├── App.tsx
│   │   ├── Router.tsx
│   │   └── Layout.tsx
│   ├── pages/                # Page components
│   │   ├── Dashboard.tsx
│   │   ├── Tasks/
│   │   │   ├── TasksPage.tsx
│   │   │   ├── TaskDetail.tsx
│   │   │   └── TodayView.tsx
│   │   ├── Batches/
│   │   ├── Labor/
│   │   ├── Compliance/
│   │   └── Settings/
│   ├── components/           # Reusable components
│   │   ├── ui/               # Base UI components (buttons, inputs, etc.)
│   │   ├── tasks/            # Task-specific components
│   │   ├── batches/
│   │   └── shared/           # Shared components (nav, header, etc.)
│   ├── hooks/                # Custom React hooks
│   │   ├── useAuth.ts
│   │   ├── useTasks.ts
│   │   └── useApi.ts
│   ├── lib/                  # Utilities and helpers
│   │   ├── api.ts            # API client
│   │   ├── auth.ts           # Auth helpers
│   │   └── utils.ts
│   ├── types/                # TypeScript types
│   │   ├── api.ts
│   │   └── models.ts
│   ├── styles/               # Global styles
│   │   ├── index.css
│   │   └── tailwind.css
│   └── main.tsx              # Entry point
├── public/                   # Static assets
├── .env.example
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md

Next.js Option

frontend/
├── app/                      # Next.js App Router
│   ├── layout.tsx
│   ├── page.tsx              # Dashboard
│   ├── tasks/
│   │   ├── page.tsx
│   │   └── [id]/page.tsx
│   ├── batches/
│   ├── labor/
│   └── api/                  # API routes (if needed)
├── components/               # Same as Vite
├── hooks/
├── lib/
├── types/
├── styles/
└── public/

Design System

Component Library (Radix UI / shadcn)

Use accessible primitives for:

  • Buttons: Primary, secondary, destructive
  • Inputs: Text, number, date, select
  • Dialogs: Modals, drawers, alerts
  • Dropdowns: Menus, selects, comboboxes
  • Tabs: Navigation, content switching
  • Cards: Content containers
  • Badges: Status indicators

Styling (Tailwind CSS)

/* Design tokens */
:root {
  --color-primary: #10b981;      /* Green for cultivation */
  --color-secondary: #3b82f6;    /* Blue for actions */
  --color-danger: #ef4444;       /* Red for alerts */
  --color-warning: #f59e0b;      /* Orange for warnings */
  --color-success: #10b981;      /* Green for success */
  
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  
  --font-sans: 'Inter', system-ui, sans-serif;
  --font-mono: 'Fira Code', monospace;
}

Mobile-First Design

  • Big Tap Targets: Minimum 44×44px for all interactive elements
  • Dark Mode: Default dark theme with light mode option
  • Responsive Breakpoints:
    • Mobile: < 640px
    • Tablet: 640px - 1024px
    • Desktop: > 1024px

Key Features

1. Today View (Mobile-Optimized)

// TodayView.tsx
export const TodayView = () => {
  const { tasks, loading } = useTasks({ dueToday: true });
  
  return (
    <div className="p-4 space-y-4">
      <h1 className="text-2xl font-bold">Today's Tasks</h1>
      {tasks.map(task => (
        <TaskCard key={task.id} task={task} />
      ))}
    </div>
  );
};

2. Timeclock Widget

// TimeclockWidget.tsx
export const TimeclockWidget = () => {
  const { currentShift, clockIn, clockOut } = useTimeclock();
  
  return (
    <Card className="p-6">
      {currentShift ? (
        <div>
          <p>Clocked in: {formatTime(currentShift.clockIn)}</p>
          <Button onClick={clockOut} size="lg">Clock Out</Button>
        </div>
      ) : (
        <Button onClick={clockIn} size="lg">Clock In</Button>
      )}
    </Card>
  );
};

3. Batch Timeline

// BatchTimeline.tsx
export const BatchTimeline = ({ batchId }: { batchId: string }) => {
  const { events } = useBatchTimeline(batchId);
  
  return (
    <div className="space-y-4">
      {events.map(event => (
        <TimelineEvent key={event.id} event={event} />
      ))}
    </div>
  );
};

API Integration

API Client

// lib/api.ts
import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add auth token to requests
api.interceptors.request.use(config => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Handle token refresh
api.interceptors.response.use(
  response => response,
  async error => {
    if (error.response?.status === 401) {
      // Attempt token refresh
      const refreshToken = localStorage.getItem('refreshToken');
      if (refreshToken) {
        const { data } = await axios.post('/api/auth/refresh', { refreshToken });
        localStorage.setItem('accessToken', data.accessToken);
        error.config.headers.Authorization = `Bearer ${data.accessToken}`;
        return axios(error.config);
      }
    }
    return Promise.reject(error);
  }
);

export default api;

Custom Hooks

// hooks/useTasks.ts
export const useTasks = (filters?: TaskFilters) => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchTasks = async () => {
      const { data } = await api.get('/api/tasks', { params: filters });
      setTasks(data.data);
      setLoading(false);
    };
    fetchTasks();
  }, [filters]);
  
  return { tasks, loading };
};

Authentication

Auth Context

// hooks/useAuth.ts
const AuthContext = createContext<AuthContextType | null>(null);

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [user, setUser] = useState<User | null>(null);
  
  const login = async (email: string, password: string) => {
    const { data } = await api.post('/api/auth/login', { email, password });
    localStorage.setItem('accessToken', data.accessToken);
    localStorage.setItem('refreshToken', data.refreshToken);
    setUser(data.user);
  };
  
  const logout = () => {
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
    setUser(null);
  };
  
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
};

Testing

Component Tests (Vitest + React Testing Library)

// TaskCard.test.tsx
import { render, screen } from '@testing-library/react';
import { TaskCard } from './TaskCard';

describe('TaskCard', () => {
  it('renders task name', () => {
    const task = { id: '1', name: 'Water plants', status: 'PENDING' };
    render(<TaskCard task={task} />);
    expect(screen.getByText('Water plants')).toBeInTheDocument();
  });
});

Environment Variables

# API
VITE_API_URL=http://localhost:3000/api

# Auth
VITE_AUTH_ENABLED=true

# Features
VITE_METRC_ENABLED=false

Development Workflow

Setup

# Install dependencies
npm install

# Set up environment variables
cp .env.example .env

# Start dev server
npm run dev

Scripts

{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "lint": "eslint src --ext .ts,.tsx",
    "format": "prettier --write src"
  }
}

Accessibility

  • WCAG 2.1 AA Compliance: All components meet accessibility standards
  • Keyboard Navigation: All interactive elements keyboard-accessible
  • Screen Reader Support: Proper ARIA labels and roles
  • Focus Management: Clear focus indicators and logical tab order
  • Color Contrast: Minimum 4.5:1 contrast ratio for text

Mobile & PWA

Progressive Web App (Stretch Goal)

// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'CA Grow Ops Manager',
        short_name: 'Grow Ops',
        theme_color: '#10b981',
        icons: [
          { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
        ],
      },
    }),
  ],
});

Next Steps

  1. Choose Build Tool: Decide between Vite and Next.js
  2. Set Up Project: Initialize React project with TypeScript
  3. Configure Tailwind: Set up Tailwind CSS and design tokens
  4. Build Design System: Implement base UI components
  5. Implement Pages: Build page components per spec
  6. Write Tests: Component and integration tests
  7. Deploy: Set up CI/CD and deploy to staging

Resources