From eb28501b2ece21c5cdd783bb5a8c0a58c890429a Mon Sep 17 00:00:00 2001 From: denshooter Date: Sun, 22 Feb 2026 00:17:08 +0100 Subject: [PATCH] Add HEIC to JPEG conversion and image resizing support; update dependencies and Dockerfile --- .claude/settings.local.json | 9 ++ Dockerfile | 4 + install.cmd | 221 +++++++++++++++++++++++++++ package-lock.json | 7 +- package.json | 3 +- src/app/admin/page.tsx | 159 +++++++++++++++---- src/app/api/files/[...path]/route.ts | 81 +++++++++- src/app/api/upload/route.ts | 18 ++- src/app/layout.tsx | 9 +- src/components/PhotoGallery.tsx | 4 +- src/components/TimelineSection.tsx | 9 +- 11 files changed, 481 insertions(+), 43 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 install.cmd diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..377d3f7 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(tail:*)", + "Bash(grep:*)", + "Bash(cd:*)" + ] + } +} diff --git a/Dockerfile b/Dockerfile index 5db73ac..fda6e34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,10 @@ COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public +# Copy sharp native binaries (needed for HEIC→JPEG conversion) +COPY --from=builder /app/node_modules/sharp ./node_modules/sharp +COPY --from=builder /app/node_modules/@img ./node_modules/@img + RUN mkdir -p /app/data/uploads/photos /app/data/uploads/videos /app/data/uploads/music \ && chown -R nextjs:nodejs /app/data diff --git a/install.cmd b/install.cmd new file mode 100644 index 0000000..7e52707 --- /dev/null +++ b/install.cmd @@ -0,0 +1,221 @@ +@echo off +setlocal enabledelayedexpansion + +REM Claude Code Windows CMD Bootstrap Script +REM Installs Claude Code for environments where PowerShell is not available + +REM Parse command line argument +set "TARGET=%~1" +if "!TARGET!"=="" set "TARGET=latest" + +REM Validate target parameter +if /i "!TARGET!"=="stable" goto :target_valid +if /i "!TARGET!"=="latest" goto :target_valid +echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul +if !ERRORLEVEL! equ 0 goto :target_valid + +echo Usage: %0 [stable^|latest^|VERSION] >&2 +echo Example: %0 1.0.58 >&2 +exit /b 1 + +:target_valid + +REM Check for 64-bit Windows +if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid +if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid +if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid +if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid + +echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2 +exit /b 1 + +:arch_valid + +REM Set constants +set "GCS_BUCKET=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" +set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads" +REM Use native ARM64 binary on ARM64 Windows, x64 otherwise +if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "PLATFORM=win32-arm64" +) else ( + set "PLATFORM=win32-x64" +) + +REM Create download directory +if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!" + +REM Check for curl availability +curl --version >nul 2>&1 +if !ERRORLEVEL! neq 0 ( + echo curl is required but not available. Please install curl or use PowerShell installer. >&2 + exit /b 1 +) + +REM Always download latest version (which has the most up-to-date installer) +call :download_file "!GCS_BUCKET!/latest" "!DOWNLOAD_DIR!\latest" +if !ERRORLEVEL! neq 0 ( + echo Failed to get latest version >&2 + exit /b 1 +) + +REM Read version from file +set /p VERSION=<"!DOWNLOAD_DIR!\latest" +del "!DOWNLOAD_DIR!\latest" + +REM Download manifest +call :download_file "!GCS_BUCKET!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json" +if !ERRORLEVEL! neq 0 ( + echo Failed to get manifest >&2 + exit /b 1 +) + +REM Extract checksum from manifest +call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!" +if !ERRORLEVEL! neq 0 ( + echo Platform !PLATFORM! not found in manifest >&2 + del "!DOWNLOAD_DIR!\manifest.json" 2>nul + exit /b 1 +) +del "!DOWNLOAD_DIR!\manifest.json" + +REM Download binary +set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe" +call :download_file "!GCS_BUCKET!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!" +if !ERRORLEVEL! neq 0 ( + echo Failed to download binary >&2 + if exist "!BINARY_PATH!" del "!BINARY_PATH!" + exit /b 1 +) + +REM Verify checksum +call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!" +if !ERRORLEVEL! neq 0 ( + echo Checksum verification failed >&2 + del "!BINARY_PATH!" + exit /b 1 +) + +REM Run claude install to set up launcher and shell integration +echo Setting up Claude Code... +"!BINARY_PATH!" install "!TARGET!" +set "INSTALL_RESULT=!ERRORLEVEL!" + +REM Clean up downloaded file +REM Wait a moment for any file handles to be released +timeout /t 1 /nobreak >nul 2>&1 +del /f "!BINARY_PATH!" >nul 2>&1 +if exist "!BINARY_PATH!" ( + echo Warning: Could not remove temporary file: !BINARY_PATH! +) + +if !INSTALL_RESULT! neq 0 ( + echo Installation failed >&2 + exit /b 1 +) + +echo. +echo Installation complete^^! +echo. +exit /b 0 + +REM ============================================================================ +REM SUBROUTINES +REM ============================================================================ + +:download_file +REM Downloads a file using curl +REM Args: %1=URL, %2=OutputPath +set "URL=%~1" +set "OUTPUT=%~2" + +curl -fsSL "!URL!" -o "!OUTPUT!" +exit /b !ERRORLEVEL! + +:parse_manifest +REM Parse JSON manifest to extract checksum for platform +REM Args: %1=ManifestPath, %2=Platform +set "MANIFEST_PATH=%~1" +set "PLATFORM_NAME=%~2" +set "EXPECTED_CHECKSUM=" + +REM Use findstr to find platform section, then look for checksum +set "FOUND_PLATFORM=" +set "IN_PLATFORM_SECTION=" + +REM Read the manifest line by line +for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do ( + set "LINE=%%i" + + REM Check if this line contains our platform + echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul + if !ERRORLEVEL! equ 0 ( + set "IN_PLATFORM_SECTION=1" + ) + + REM If we're in the platform section, look for checksum + if defined IN_PLATFORM_SECTION ( + echo !LINE! | findstr /c:"\"checksum\":" >nul + if !ERRORLEVEL! equ 0 ( + REM Extract checksum value + for /f "tokens=2 delims=:" %%j in ("!LINE!") do ( + set "CHECKSUM_PART=%%j" + REM Remove quotes, whitespace, and comma + set "CHECKSUM_PART=!CHECKSUM_PART: =!" + set "CHECKSUM_PART=!CHECKSUM_PART:"=!" + set "CHECKSUM_PART=!CHECKSUM_PART:,=!" + + REM Check if it looks like a SHA256 (64 hex chars) + if not "!CHECKSUM_PART!"=="" ( + call :check_length "!CHECKSUM_PART!" 64 + if !ERRORLEVEL! equ 0 ( + set "EXPECTED_CHECKSUM=!CHECKSUM_PART!" + exit /b 0 + ) + ) + ) + ) + + REM Check if we've left the platform section (closing brace) + echo !LINE! | findstr /c:"}" >nul + if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION=" + ) +) + +if "!EXPECTED_CHECKSUM!"=="" exit /b 1 +exit /b 0 + +:check_length +REM Check if string length equals expected length +REM Args: %1=String, %2=ExpectedLength +set "STR=%~1" +set "EXPECTED_LEN=%~2" +set "LEN=0" +:count_loop +if "!STR:~%LEN%,1!"=="" goto :count_done +set /a LEN+=1 +goto :count_loop +:count_done +if %LEN%==%EXPECTED_LEN% exit /b 0 +exit /b 1 + +:verify_checksum +REM Verify file checksum using certutil +REM Args: %1=FilePath, %2=ExpectedChecksum +set "FILE_PATH=%~1" +set "EXPECTED=%~2" + +for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do ( + set "ACTUAL=%%i" + set "ACTUAL=!ACTUAL: =!" + if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done + if "!ACTUAL!" neq "" ( + if /i "!ACTUAL!"=="!EXPECTED!" ( + exit /b 0 + ) else ( + exit /b 1 + ) + ) +) + +:verify_done +exit /b 1 diff --git a/package-lock.json b/package-lock.json index 93cff4f..66e96fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "next": "^16.1.6", "qrcode": "^1.5.4", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "sharp": "^0.34.5" }, "devDependencies": { "@types/node": "^22", @@ -497,7 +498,6 @@ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", - "optional": true, "engines": { "node": ">=18" } @@ -2051,7 +2051,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -3169,7 +3168,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -3189,7 +3187,6 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", diff --git a/package.json b/package.json index b79f7a8..57b30c6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "next": "^16.1.6", "qrcode": "^1.5.4", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "sharp": "^0.34.5" }, "devDependencies": { "@types/node": "^22", diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index d5652b9..054f470 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1187,34 +1187,141 @@ export default function AdminPage() { {timelineContributions .filter(c => c.type === 'timeline') .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) - .map(c => ( -
-
-
- {c.title || 'Ohne Titel'} - {c.year && {c.day ? `${c.day}.` : ''}{c.month ? `${c.month}.` : ''}{c.year}} - {c.status === 'flagged' && 🚩} - {c.status === 'approved' && } - {c.status === 'rejected' && } -
-

{c.name} {c.content ? `· ${c.content}` : ''}

-
-
- {(c.status === 'pending' || c.status === 'flagged') && ( - <> - - - + .map(c => { + const isEditing = editingContribution?.id === c.id + return ( +
+ {isEditing ? ( +
+
+ setEditingContribution({ ...editingContribution, day: e.target.value })} + placeholder="Tag" + className="px-2 py-1.5 rounded border border-warm-border bg-white text-warm-brown text-xs" + /> + setEditingContribution({ ...editingContribution, month: e.target.value })} + placeholder="Monat" + className="px-2 py-1.5 rounded border border-warm-border bg-white text-warm-brown text-xs" + /> + setEditingContribution({ ...editingContribution, year: e.target.value })} + placeholder="Jahr" + className="px-2 py-1.5 rounded border border-warm-border bg-white text-warm-brown text-xs" + /> +
+ setEditingContribution({ ...editingContribution, title: e.target.value })} + placeholder="Titel" + className="w-full px-2 py-1.5 rounded border border-warm-border bg-white text-warm-brown text-xs" + /> + setEditingContribution({ ...editingContribution, location: e.target.value })} + placeholder="Ort (optional)" + className="w-full px-2 py-1.5 rounded border border-warm-border bg-white text-warm-brown text-xs" + /> +