diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e2444b5..0000000 --- a/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -FROM node:20-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Install dependencies -COPY package.json package-lock.json* ./ -RUN npm ci - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Next.js collects anonymous telemetry data about general usage -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN npm run build - -# Production image, copy all the files and run next -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -# Copy necessary files -# Note: Automatically copy public folder. Next.js standalone build includes public in .next/standalone -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" - -CMD ["node", "server.js"] diff --git a/Dockerfile.jsonserver b/Dockerfile.jsonserver deleted file mode 100644 index d459dc7..0000000 --- a/Dockerfile.jsonserver +++ /dev/null @@ -1,19 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -# Install json-server globally -RUN npm install -g json-server@0.17.4 - -# Create data directory -RUN mkdir -p /data - -# Copy initial db.json -COPY db.json /data/db.json - -EXPOSE 3001 - -# Use volume mount for persistence -VOLUME ["/data"] - -CMD ["json-server", "--watch", "/data/db.json", "--host", "0.0.0.0", "--port", "3001"] diff --git a/README.md b/README.md index 86aa9a5..0ce95fb 100644 --- a/README.md +++ b/README.md @@ -29,39 +29,6 @@ npm run dev 4. Open [http://localhost:3000](http://localhost:3000) in your browser -### Docker Deployment - -#### Local Testing with Docker Compose -```bash -docker-compose up -d -``` - -#### Docker Swarm Deployment - -1. Build the images: -```bash -./deploy.sh -``` - -2. Deploy to swarm: -```bash -docker stack deploy -c docker-compose.swarm.yml presence-tracker -``` - -3. Check the deployment: -```bash -docker stack services presence-tracker -``` - -4. Remove the stack: -```bash -docker stack rm presence-tracker -``` - -The application will be available at: -- Web UI: http://localhost:3000 -- JSON API: http://localhost:3001 - ## Features - Add workers with their presence days diff --git a/app/page.tsx b/app/page.tsx index ccbc80a..e17d9fb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -17,16 +17,38 @@ export default function Home() { const [showResetConfirm, setShowResetConfirm] = useState(false); const [editTotalPoints, setEditTotalPoints] = useState(''); const [editTotalDays, setEditTotalDays] = useState(''); + const [darkMode, setDarkMode] = useState(false); + const [mounted, setMounted] = useState(false); const TOTAL_POINTS = settings.totalPoints; const TOTAL_DAYS = settings.totalDays; const POINTS_PER_DAY = TOTAL_POINTS / TOTAL_DAYS; useEffect(() => { + setMounted(true); loadWorkers(); loadSettings(); + + // Load dark mode preference from localStorage + const savedDarkMode = localStorage.getItem('darkMode'); + if (savedDarkMode !== null) { + setDarkMode(savedDarkMode === 'true'); + } }, []); + useEffect(() => { + if (!mounted) return; + + // Apply dark mode class to document root + if (darkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + // Save preference to localStorage + localStorage.setItem('darkMode', darkMode.toString()); + }, [darkMode, mounted]); + const loadWorkers = async () => { try { const data = await workersAPI.getAll(); @@ -188,27 +210,31 @@ export default function Home() { }; return ( -
-
+
+
-

Worker Presence Manager

-

- Sprint duration: {TOTAL_DAYS} working days | Total points per worker: {TOTAL_POINTS} +

Worker Presence Manager

+

+ Sprint duration: {TOTAL_DAYS} working days | Total points per worker: {TOTAL_POINTS}

{/* Add Worker Form */} -
-

Add Worker

+
+

Add Worker

e.key === 'Enter' && addWorker()} /> @@ -235,17 +261,17 @@ export default function Home() { }} min="0" max={TOTAL_DAYS} - className={`w-full sm:w-48 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 ${ + className={`w-full sm:w-48 px-4 py-2.5 border rounded-lg focus:outline-none focus:ring-2 transition-all bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 ${ daysError - ? 'border-red-500 focus:ring-red-500' - : 'border-gray-300 focus:ring-blue-500' + ? 'border-red-400 focus:ring-red-400 focus:border-red-400' + : 'border-slate-300 dark:border-slate-600 focus:ring-indigo-500 focus:border-indigo-500' }`} onKeyPress={(e) => e.key === 'Enter' && addWorker()} /> @@ -254,32 +280,32 @@ export default function Home() { {/* Workers List */} {workers.length > 0 ? ( -
+
- + - - - - - - - + {workers.map((worker) => { const pointsEarned = calculatePoints(worker.presenceDays); const pointsUsed = calculateUsedPoints(worker.presenceDays); @@ -287,11 +313,11 @@ export default function Home() { const isEditing = editingId === worker.id; return ( - - + - - - -
+ Worker Name + Presence Days + Points Earned + Points Used + Remaining Points + Actions
+
{worker.name} + {isEditing ? ( setEditingDays(e.target.value)} min="0" max={TOTAL_DAYS} - className="w-20 px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-20 px-2 py-1.5 border border-indigo-300 dark:border-indigo-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100" onKeyPress={(e) => { if (e.key === 'Enter') saveEditing(worker.id); if (e.key === 'Escape') cancelEditing(); @@ -310,13 +336,13 @@ export default function Home() { `${worker.presenceDays} / ${TOTAL_DAYS}` )} + {pointsEarned.toFixed(2)} + {pointsUsed.toFixed(2)} + {remainingPoints.toFixed(2)} / {TOTAL_POINTS} @@ -324,19 +350,19 @@ export default function Home() {
@@ -345,7 +371,7 @@ export default function Home() {
{/* Summary */} -
+
- Total Workers: - {workers.length} + Total Workers: + {workers.length}
- Avg Presence Days: - + Avg Presence Days: + {workers.length > 0 ? (workers.reduce((sum, w) => sum + w.presenceDays, 0) / workers.length).toFixed(1) : '0'}
- Total Presence Days: - + Total Presence Days: + {workers.reduce((sum, w) => sum + w.presenceDays, 0)}
- Total Points in the sprint: - + Total Points in the sprint: + {workers.reduce((sum, w) => sum + calculatePoints(w.presenceDays), 0).toFixed(2)}
) : ( -
-

No workers added yet. Add your first worker above.

+
+ + + +

No workers added yet. Add your first worker above.

)} {/* Info Box */} -
-

How it works:

-
    -
  • • Each worker has {TOTAL_POINTS} points for a sprint ({TOTAL_DAYS} working days)
  • -
  • • Each presence day equals {POINTS_PER_DAY.toFixed(2)} points
  • -
  • • Points Earned = Presence Days × {POINTS_PER_DAY.toFixed(2)}
  • -
  • • Points Used = {TOTAL_POINTS} - Points Earned (days not present)
  • -
  • • Remaining Points = Points Earned
  • +
    +

    + + + + How it works: +

    +
      +
    • + + Each worker has {TOTAL_POINTS} points for a sprint ({TOTAL_DAYS} working days) +
    • +
    • + + Each presence day equals {POINTS_PER_DAY.toFixed(2)} points +
    • +
    • + + Points Earned = Presence Days × {POINTS_PER_DAY.toFixed(2)} +
    • +
    • + + Points Used = {TOTAL_POINTS} - Points Earned (days not present) +
    • +
    • + + Remaining Points = Points Earned +
    {/* Settings Modal */} {showSettings && ( -
    -
    -

    Settings

    +
    +
    +

    Settings

    + +
    + {/* Dark Mode Toggle */} +
    +
    +
    +
    + {darkMode ? ( + + + + ) : ( + + + + )} +
    +
    + Dark Mode + Toggle theme appearance +
    +
    + +
    +
    -
    -
    -
    -
    +

    - Points per day:{' '} - {editTotalPoints && editTotalDays && parseInt(editTotalPoints) > 0 && parseInt(editTotalDays) > 0 - ? (parseInt(editTotalPoints) / parseInt(editTotalDays)).toFixed(2) - : 'N/A'} + Points per day:{' '} + + {editTotalPoints && editTotalDays && parseInt(editTotalPoints) > 0 && parseInt(editTotalDays) > 0 + ? (parseInt(editTotalPoints) / parseInt(editTotalDays)).toFixed(2) + : 'N/A'} +

    @@ -486,14 +577,14 @@ export default function Home() { @@ -504,18 +595,18 @@ export default function Home() { {/* Reset Confirmation Modal */} {showResetConfirm && ( -
    -
    +
    +
    -
    - +
    +
    -

    Reset All Presences

    +

    Reset All Presences

    -

    +

    Are you sure you want to reset all worker presences to 0? This action cannot be undone.

    @@ -523,14 +614,14 @@ export default function Home() { diff --git a/docker-compose.swarm.yml b/docker-compose.swarm.yml deleted file mode 100644 index 26a08da..0000000 --- a/docker-compose.swarm.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: '3.8' - -services: - json-server: - image: presence-tracker-api:latest - ports: - - "3001:3001" - volumes: - - json-data:/data - networks: - - presence-network - deploy: - replicas: 1 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - placement: - constraints: - - node.role == worker - - nextjs: - image: presence-tracker-web:latest - ports: - - "3000:3000" - environment: - - API_URL=http://json-server:3001 - networks: - - presence-network - deploy: - replicas: 2 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - update_config: - parallelism: 1 - delay: 10s - placement: - constraints: - - node.role == worker - depends_on: - - json-server - -volumes: - json-data: - driver: local - -networks: - presence-network: - driver: overlay diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a5f7b8a..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: '3.8' - -services: - json-server: - build: - context: . - dockerfile: Dockerfile.jsonserver - container_name: presence-tracker-api - ports: - - "3001:3001" - volumes: - - json-data:/data - networks: - - presence-network - restart: unless-stopped - - nextjs: - build: - context: . - dockerfile: Dockerfile - container_name: presence-tracker-web - ports: - - "3000:3000" - environment: - - API_URL=http://json-server:3001 - depends_on: - - json-server - networks: - - presence-network - restart: unless-stopped - -volumes: - json-data: - driver: local - -networks: - presence-network: - driver: bridge diff --git a/tailwind.config.ts b/tailwind.config.ts index 35ba2d0..3bda38e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -6,6 +6,7 @@ const config: Config = { "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", ], + darkMode: 'class', theme: { extend: {}, },