Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fec7b687f7 | |||
| cfa29ad5cb | |||
| 76ae35217c | |||
| fe651fa38e | |||
| f7b8c0c0fe |
@@ -14,6 +14,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
new_version: ${{ steps.version.outputs.new_version }}
|
new_version: ${{ steps.version.outputs.new_version }}
|
||||||
version_changed: ${{ steps.version.outputs.version_changed }}
|
version_changed: ${{ steps.version.outputs.version_changed }}
|
||||||
|
component_changed: ${{ steps.version.outputs.component_changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
@@ -30,15 +31,104 @@ jobs:
|
|||||||
git config --global user.name 'Gitea Actions Bot'
|
git config --global user.name 'Gitea Actions Bot'
|
||||||
git config --global user.email 'actions@gitea.local'
|
git config --global user.email 'actions@gitea.local'
|
||||||
|
|
||||||
- name: Bump version
|
- name: Detect changes and bump version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
# Get current version from backend package.json
|
set -e # Exit on error
|
||||||
CURRENT_VERSION=$(node -p "require('./backend/package.json').version")
|
|
||||||
echo "Current version: $CURRENT_VERSION"
|
|
||||||
|
|
||||||
# Split version into parts
|
echo "=== Debug Info ==="
|
||||||
IFS='.' read -r -a version_parts <<< "$CURRENT_VERSION"
|
echo "GitHub event before: ${{ github.event.before }}"
|
||||||
|
echo "GitHub SHA: ${{ github.sha }}"
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo "Git log (last 5): $(git log --oneline -5)"
|
||||||
|
|
||||||
|
# Get the commit range for changed files
|
||||||
|
if [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ] && [ "${{ github.event.before }}" != "" ]; then
|
||||||
|
COMMIT_RANGE="${{ github.event.before }}..${{ github.sha }}"
|
||||||
|
echo "Using commit range: $COMMIT_RANGE"
|
||||||
|
CHANGED_FILES=$(git diff --name-only $COMMIT_RANGE || echo "")
|
||||||
|
else
|
||||||
|
# First commit or no previous commit, check against HEAD~1 if it exists
|
||||||
|
if git rev-parse HEAD~1 >/dev/null 2>&1; then
|
||||||
|
COMMIT_RANGE="HEAD~1..HEAD"
|
||||||
|
echo "Using commit range: $COMMIT_RANGE"
|
||||||
|
CHANGED_FILES=$(git diff --name-only $COMMIT_RANGE || echo "")
|
||||||
|
else
|
||||||
|
echo "First commit detected, checking all files"
|
||||||
|
CHANGED_FILES=$(git ls-files)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Changed files:"
|
||||||
|
echo "$CHANGED_FILES"
|
||||||
|
|
||||||
|
# Check what changed (using echo to pipe to grep to avoid grep exit codes)
|
||||||
|
BACKEND_CHANGED=$(echo "$CHANGED_FILES" | grep -c '^backend/' || echo "0")
|
||||||
|
FRONTEND_CHANGED=$(echo "$CHANGED_FILES" | grep -c '^frontend/' || echo "0")
|
||||||
|
ROOT_CHANGED=$(echo "$CHANGED_FILES" | grep -c -E '^(package\.json|docker-compose|Dockerfile|scripts/)' || echo "0")
|
||||||
|
|
||||||
|
echo "Backend files changed: $BACKEND_CHANGED"
|
||||||
|
echo "Frontend files changed: $FRONTEND_CHANGED"
|
||||||
|
echo "Root files changed: $ROOT_CHANGED"
|
||||||
|
|
||||||
|
# Get current versions
|
||||||
|
BACKEND_VERSION=$(node -p "require('./backend/package.json').version" 2>/dev/null || echo "1.0.0")
|
||||||
|
FRONTEND_VERSION=$(node -p "require('./frontend/package.json').version" 2>/dev/null || echo "1.0.0")
|
||||||
|
|
||||||
|
echo "Current backend version: $BACKEND_VERSION"
|
||||||
|
echo "Current frontend version: $FRONTEND_VERSION"
|
||||||
|
|
||||||
|
# Determine what to update based on changes
|
||||||
|
BACKEND_UPDATE=false
|
||||||
|
FRONTEND_UPDATE=false
|
||||||
|
COMPONENT_CHANGED="none"
|
||||||
|
|
||||||
|
if [ "$ROOT_CHANGED" -gt 0 ]; then
|
||||||
|
# Root changes affect both components
|
||||||
|
BACKEND_UPDATE=true
|
||||||
|
FRONTEND_UPDATE=true
|
||||||
|
COMPONENT_CHANGED="both"
|
||||||
|
SOURCE_VERSION=$BACKEND_VERSION
|
||||||
|
echo "Root changes detected - updating both components"
|
||||||
|
elif [ "$BACKEND_CHANGED" -gt 0 ] && [ "$FRONTEND_CHANGED" -gt 0 ]; then
|
||||||
|
# Both components changed
|
||||||
|
BACKEND_UPDATE=true
|
||||||
|
FRONTEND_UPDATE=true
|
||||||
|
COMPONENT_CHANGED="both"
|
||||||
|
# Use the higher version as source
|
||||||
|
if [ "$(printf '%s\n' "$BACKEND_VERSION" "$FRONTEND_VERSION" | sort -V | tail -n1)" = "$BACKEND_VERSION" ]; then
|
||||||
|
SOURCE_VERSION=$BACKEND_VERSION
|
||||||
|
else
|
||||||
|
SOURCE_VERSION=$FRONTEND_VERSION
|
||||||
|
fi
|
||||||
|
echo "Both backend and frontend changed - updating both"
|
||||||
|
elif [ "$BACKEND_CHANGED" -gt 0 ]; then
|
||||||
|
# Only backend changed
|
||||||
|
BACKEND_UPDATE=true
|
||||||
|
COMPONENT_CHANGED="backend"
|
||||||
|
SOURCE_VERSION=$BACKEND_VERSION
|
||||||
|
echo "Only backend changed - updating backend"
|
||||||
|
elif [ "$FRONTEND_CHANGED" -gt 0 ]; then
|
||||||
|
# Only frontend changed
|
||||||
|
FRONTEND_UPDATE=true
|
||||||
|
COMPONENT_CHANGED="frontend"
|
||||||
|
SOURCE_VERSION=$FRONTEND_VERSION
|
||||||
|
echo "Only frontend changed - updating frontend"
|
||||||
|
else
|
||||||
|
echo "No relevant changes detected"
|
||||||
|
echo "version_changed=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "component_changed=none" >> $GITHUB_OUTPUT
|
||||||
|
echo "new_version=" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Component changed: $COMPONENT_CHANGED"
|
||||||
|
echo "Source version: $SOURCE_VERSION"
|
||||||
|
echo "Backend update: $BACKEND_UPDATE"
|
||||||
|
echo "Frontend update: $FRONTEND_UPDATE"
|
||||||
|
|
||||||
|
# Calculate new version
|
||||||
|
IFS='.' read -r -a version_parts <<< "$SOURCE_VERSION"
|
||||||
MAJOR="${version_parts[0]}"
|
MAJOR="${version_parts[0]}"
|
||||||
MINOR="${version_parts[1]}"
|
MINOR="${version_parts[1]}"
|
||||||
PATCH="${version_parts[2]}"
|
PATCH="${version_parts[2]}"
|
||||||
@@ -49,14 +139,23 @@ jobs:
|
|||||||
|
|
||||||
echo "New version: $NEW_VERSION"
|
echo "New version: $NEW_VERSION"
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "component_changed=$COMPONENT_CHANGED" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Update version in package.json files
|
# Update versions in package.json files
|
||||||
cd backend && npm version $NEW_VERSION --no-git-tag-version
|
if [ "$BACKEND_UPDATE" = true ]; then
|
||||||
cd ../frontend && npm version $NEW_VERSION --no-git-tag-version
|
echo "Updating backend version to $NEW_VERSION"
|
||||||
cd ..
|
cd backend && npm version $NEW_VERSION --no-git-tag-version
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if there are changes
|
if [ "$FRONTEND_UPDATE" = true ]; then
|
||||||
if [[ -n $(git status -s) ]]; then
|
echo "Updating frontend version to $NEW_VERSION"
|
||||||
|
cd frontend && npm version $NEW_VERSION --no-git-tag-version
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there are changes to commit
|
||||||
|
if [[ -n $(git status --porcelain) ]]; then
|
||||||
echo "version_changed=true" >> $GITHUB_OUTPUT
|
echo "version_changed=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "version_changed=false" >> $GITHUB_OUTPUT
|
echo "version_changed=false" >> $GITHUB_OUTPUT
|
||||||
@@ -65,15 +164,36 @@ jobs:
|
|||||||
- name: Commit version bump
|
- name: Commit version bump
|
||||||
if: steps.version.outputs.version_changed == 'true'
|
if: steps.version.outputs.version_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
git add backend/package.json backend/package-lock.json
|
COMPONENT="${{ steps.version.outputs.component_changed }}"
|
||||||
git add frontend/package.json frontend/package-lock.json
|
|
||||||
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}"
|
if [ "$COMPONENT" = "both" ]; then
|
||||||
|
git add backend/package.json backend/package-lock.json
|
||||||
|
git add frontend/package.json frontend/package-lock.json
|
||||||
|
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }} (backend + frontend)"
|
||||||
|
elif [ "$COMPONENT" = "backend" ]; then
|
||||||
|
git add backend/package.json backend/package-lock.json
|
||||||
|
git commit -m "chore: bump backend version to ${{ steps.version.outputs.new_version }}"
|
||||||
|
elif [ "$COMPONENT" = "frontend" ]; then
|
||||||
|
git add frontend/package.json frontend/package-lock.json
|
||||||
|
git commit -m "chore: bump frontend version to ${{ steps.version.outputs.new_version }}"
|
||||||
|
fi
|
||||||
|
|
||||||
git push
|
git push
|
||||||
|
|
||||||
- name: Create Git tag
|
- name: Create Git tag
|
||||||
if: steps.version.outputs.version_changed == 'true'
|
if: steps.version.outputs.version_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
git tag -a "v${{ steps.version.outputs.new_version }}" -m "Release v${{ steps.version.outputs.new_version }}"
|
COMPONENT="${{ steps.version.outputs.component_changed }}"
|
||||||
|
|
||||||
|
if [ "$COMPONENT" = "both" ]; then
|
||||||
|
TAG_MESSAGE="Release v${{ steps.version.outputs.new_version }} (backend + frontend)"
|
||||||
|
elif [ "$COMPONENT" = "backend" ]; then
|
||||||
|
TAG_MESSAGE="Release v${{ steps.version.outputs.new_version }} (backend)"
|
||||||
|
elif [ "$COMPONENT" = "frontend" ]; then
|
||||||
|
TAG_MESSAGE="Release v${{ steps.version.outputs.new_version }} (frontend)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag -a "v${{ steps.version.outputs.new_version }}" -m "$TAG_MESSAGE"
|
||||||
git push origin "v${{ steps.version.outputs.new_version }}"
|
git push origin "v${{ steps.version.outputs.new_version }}"
|
||||||
|
|
||||||
trigger-drone:
|
trigger-drone:
|
||||||
@@ -84,5 +204,6 @@ jobs:
|
|||||||
- name: Trigger Drone Build
|
- name: Trigger Drone Build
|
||||||
run: |
|
run: |
|
||||||
echo "Version bumped to ${{ needs.version-bump.outputs.new_version }}"
|
echo "Version bumped to ${{ needs.version-bump.outputs.new_version }}"
|
||||||
|
echo "Component(s) changed: ${{ needs.version-bump.outputs.component_changed }}"
|
||||||
echo "Drone will automatically trigger on the new tag"
|
echo "Drone will automatically trigger on the new tag"
|
||||||
# Drone CI will automatically trigger on the tag push event
|
# Drone CI will automatically trigger on the tag push event
|
||||||
Binary file not shown.
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.45",
|
"version": "1.0.47",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.45",
|
"version": "1.0.47",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.45",
|
"version": "1.0.47",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -393,13 +393,56 @@ router.delete('/:id', adminAuth, async (req, res) => {
|
|||||||
return res.status(404).json({ error: 'Event not found' });
|
return res.status(404).json({ error: 'Event not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete associated photos
|
// Start a transaction to ensure all deletions succeed or fail together
|
||||||
await db('photos').where('event_id', id).del();
|
await db.transaction(async (trx) => {
|
||||||
|
// 1. Delete activity logs (audit trail)
|
||||||
|
await trx('activity_logs').where('event_id', id).del();
|
||||||
|
|
||||||
// Delete event
|
// 2. Delete access logs
|
||||||
await db('events').where('id', id).del();
|
await trx('access_logs').where('event_id', id).del();
|
||||||
|
|
||||||
// Log activity
|
// 3. Delete email queue entries
|
||||||
|
await trx('email_queue').where('event_id', id).del();
|
||||||
|
|
||||||
|
// 4. Delete photos (this will also handle hero_photo_id foreign key)
|
||||||
|
await trx('photos').where('event_id', id).del();
|
||||||
|
|
||||||
|
// 5. Delete categories (photo_categories has CASCADE delete for event_id)
|
||||||
|
await trx('photo_categories').where('event_id', id).del();
|
||||||
|
|
||||||
|
// 6. Finally delete the event
|
||||||
|
await trx('events').where('id', id).del();
|
||||||
|
|
||||||
|
// Delete event folder from storage if it exists
|
||||||
|
if (event.folder_path) {
|
||||||
|
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||||
|
const eventFolderPath = path.join(storagePath, 'events', 'active', event.folder_path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fsPromises = require('fs').promises;
|
||||||
|
await fsPromises.rm(eventFolderPath, { recursive: true, force: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete event folder:', err);
|
||||||
|
// Don't fail the transaction if folder deletion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete archive if exists
|
||||||
|
if (event.archive_path) {
|
||||||
|
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||||
|
const archivePath = path.join(storagePath, event.archive_path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fsPromises = require('fs').promises;
|
||||||
|
await fsPromises.unlink(archivePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete archive file:', err);
|
||||||
|
// Don't fail the transaction if file deletion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log activity (outside transaction)
|
||||||
await logActivity('event_deleted',
|
await logActivity('event_deleted',
|
||||||
{ event_name: event.event_name },
|
{ event_name: event.event_name },
|
||||||
null,
|
null,
|
||||||
@@ -409,7 +452,19 @@ router.delete('/:id', adminAuth, async (req, res) => {
|
|||||||
res.json({ message: 'Event deleted successfully' });
|
res.json({ message: 'Event deleted successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting event:', error);
|
console.error('Error deleting event:', error);
|
||||||
res.status(500).json({ error: 'Failed to delete event' });
|
|
||||||
|
// Provide more specific error messages
|
||||||
|
if (error.message && error.message.includes('foreign key constraint')) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Cannot delete event due to existing references. Please contact support.',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to delete event',
|
||||||
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,17 @@ const { db } = require('../database/db');
|
|||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
let transporter = null;
|
let transporter = null;
|
||||||
|
let lastConfigHash = null;
|
||||||
|
|
||||||
|
// Generate hash from config for change detection
|
||||||
|
function generateConfigHash(config) {
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const configString = `${config.smtp_host}:${config.smtp_port}:${config.smtp_user}:${config.smtp_pass}:${config.smtp_secure}`;
|
||||||
|
return crypto.createHash('md5').update(configString).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize transporter from database config
|
// Initialize transporter from database config
|
||||||
async function initializeTransporter() {
|
async function initializeTransporter(forceReinit = false) {
|
||||||
try {
|
try {
|
||||||
const config = await db('email_configs').first();
|
const config = await db('email_configs').first();
|
||||||
|
|
||||||
@@ -14,6 +22,16 @@ async function initializeTransporter() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if configuration has changed
|
||||||
|
const currentConfigHash = generateConfigHash(config);
|
||||||
|
if (!forceReinit && transporter && currentConfigHash === lastConfigHash) {
|
||||||
|
// Configuration hasn't changed, return existing transporter
|
||||||
|
return transporter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration has changed or first initialization
|
||||||
|
logger.info('Initializing email transporter' + (lastConfigHash && currentConfigHash !== lastConfigHash ? ' (configuration changed)' : ''));
|
||||||
|
|
||||||
transporter = nodemailer.createTransport({
|
transporter = nodemailer.createTransport({
|
||||||
host: config.smtp_host,
|
host: config.smtp_host,
|
||||||
port: config.smtp_port,
|
port: config.smtp_port,
|
||||||
@@ -28,9 +46,14 @@ async function initializeTransporter() {
|
|||||||
await transporter.verify();
|
await transporter.verify();
|
||||||
logger.info('Email transporter initialized successfully');
|
logger.info('Email transporter initialized successfully');
|
||||||
|
|
||||||
|
// Update the config hash
|
||||||
|
lastConfigHash = currentConfigHash;
|
||||||
|
|
||||||
return transporter;
|
return transporter;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to initialize email transporter:', error);
|
logger.error('Failed to initialize email transporter:', error);
|
||||||
|
transporter = null;
|
||||||
|
lastConfigHash = null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,11 +279,10 @@ async function processTemplate(template, variables, language = 'en') {
|
|||||||
// Send email using template
|
// Send email using template
|
||||||
async function sendTemplateEmail(to, templateKey, variables) {
|
async function sendTemplateEmail(to, templateKey, variables) {
|
||||||
try {
|
try {
|
||||||
|
// Always check for configuration changes before sending
|
||||||
|
transporter = await initializeTransporter();
|
||||||
if (!transporter) {
|
if (!transporter) {
|
||||||
transporter = await initializeTransporter();
|
throw new Error('Email service not configured');
|
||||||
if (!transporter) {
|
|
||||||
throw new Error('Email service not configured');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get email template
|
// Get email template
|
||||||
@@ -304,6 +326,16 @@ async function sendTemplateEmail(to, templateKey, variables) {
|
|||||||
// Process email queue
|
// Process email queue
|
||||||
async function processEmailQueue() {
|
async function processEmailQueue() {
|
||||||
try {
|
try {
|
||||||
|
// Try to initialize transporter if it's null (in case it failed at startup)
|
||||||
|
if (!transporter) {
|
||||||
|
logger.info('Transporter not initialized, attempting to initialize...');
|
||||||
|
transporter = await initializeTransporter();
|
||||||
|
if (!transporter) {
|
||||||
|
logger.warn('Email transporter could not be initialized, skipping queue processing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pendingEmails = await db('email_queue')
|
const pendingEmails = await db('email_queue')
|
||||||
.where('status', 'pending')
|
.where('status', 'pending')
|
||||||
.where('retry_count', '<', 3)
|
.where('retry_count', '<', 3)
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.45",
|
"version": "1.0.47",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.45",
|
"version": "1.0.47",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-link": "^2.25.0",
|
"@tiptap/extension-link": "^2.25.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.45",
|
"version": "1.0.47",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
import { api } from '../../config/api';
|
import { api } from '../../config/api';
|
||||||
|
import packageJson from '../../../package.json';
|
||||||
|
|
||||||
// Frontend version from package.json
|
// Frontend version from package.json
|
||||||
const FRONTEND_VERSION = '1.0.0';
|
const FRONTEND_VERSION = packageJson.version;
|
||||||
|
|
||||||
interface SystemVersion {
|
interface SystemVersion {
|
||||||
backend: string;
|
backend: string;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{rightIcon && (
|
{rightIcon && (
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||||
<span className="text-neutral-500">{rightIcon}</span>
|
<span className="text-neutral-500">{rightIcon}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -264,6 +264,7 @@
|
|||||||
"adminEmailHelp": "Erhält Systembenachrichtigungen und Archivbestätigungen",
|
"adminEmailHelp": "Erhält Systembenachrichtigungen und Archivbestätigungen",
|
||||||
"securityAccess": "Sicherheit & Zugriff",
|
"securityAccess": "Sicherheit & Zugriff",
|
||||||
"galleryPassword": "Galerie-Passwort",
|
"galleryPassword": "Galerie-Passwort",
|
||||||
|
"passwordHelperText": "Sie können Datumsangaben wie \"04.07.2025\" oder beliebigen Text mit mindestens 6 Zeichen verwenden",
|
||||||
"passwordPlaceholder": "Sicheres Passwort eingeben",
|
"passwordPlaceholder": "Sicheres Passwort eingeben",
|
||||||
"confirmPassword": "Passwort bestätigen",
|
"confirmPassword": "Passwort bestätigen",
|
||||||
"showPasswords": "Passwörter anzeigen",
|
"showPasswords": "Passwörter anzeigen",
|
||||||
@@ -363,7 +364,13 @@
|
|||||||
"eventsSelected_plural": "{{count}} Veranstaltungen ausgewählt",
|
"eventsSelected_plural": "{{count}} Veranstaltungen ausgewählt",
|
||||||
"bulkArchive": "Archivieren",
|
"bulkArchive": "Archivieren",
|
||||||
"confirmBulkArchive": "Sind Sie sicher, dass Sie {{count}} Veranstaltung(en) archivieren möchten?",
|
"confirmBulkArchive": "Sind Sie sicher, dass Sie {{count}} Veranstaltung(en) archivieren möchten?",
|
||||||
"confirmBulkArchiveDescription": "Diese Aktion kann nicht rückgängig gemacht werden. Archivierte Veranstaltungen sind nicht mehr öffentlich zugänglich."
|
"confirmBulkArchiveDescription": "Diese Aktion kann nicht rückgängig gemacht werden. Archivierte Veranstaltungen sind nicht mehr öffentlich zugänglich.",
|
||||||
|
"stats": {
|
||||||
|
"totalEvents": "Gesamtveranstaltungen",
|
||||||
|
"activeEvents": "Aktive Veranstaltungen",
|
||||||
|
"totalPhotos": "Gesamtfotos",
|
||||||
|
"expiringEvents": "Bald ablaufend"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Systemeinstellungen",
|
"title": "Systemeinstellungen",
|
||||||
|
|||||||
@@ -282,6 +282,7 @@
|
|||||||
"adminEmailHelp": "Will receive system notifications and archive confirmations",
|
"adminEmailHelp": "Will receive system notifications and archive confirmations",
|
||||||
"securityAccess": "Security & Access",
|
"securityAccess": "Security & Access",
|
||||||
"galleryPassword": "Gallery Password",
|
"galleryPassword": "Gallery Password",
|
||||||
|
"passwordHelperText": "You can use dates like \"04.07.2025\" or any text with 6+ characters",
|
||||||
"confirmPassword": "Confirm Password",
|
"confirmPassword": "Confirm Password",
|
||||||
"showPasswords": "Show passwords",
|
"showPasswords": "Show passwords",
|
||||||
"gallerySettings": "Gallery Settings",
|
"gallerySettings": "Gallery Settings",
|
||||||
@@ -340,6 +341,12 @@
|
|||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"noEventsFound": "No events found",
|
"noEventsFound": "No events found",
|
||||||
|
"stats": {
|
||||||
|
"totalEvents": "Total Events",
|
||||||
|
"activeEvents": "Active Events",
|
||||||
|
"totalPhotos": "Total Photos",
|
||||||
|
"expiringEvents": "Expiring Soon"
|
||||||
|
},
|
||||||
"viewDetails": "View Details",
|
"viewDetails": "View Details",
|
||||||
"archiveEventAction": "Archive Event",
|
"archiveEventAction": "Archive Event",
|
||||||
"downloadArchiveAction": "Download Archive",
|
"downloadArchiveAction": "Download Archive",
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export const ArchivesPage: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-neutral-600">{t('archives.totalPhotos')}</p>
|
<p className="text-sm text-neutral-600">{t('archives.totalPhotos')}</p>
|
||||||
<p className="text-2xl font-bold text-neutral-900">
|
<p className="text-2xl font-bold text-neutral-900">
|
||||||
{archives.reduce((sum, a) => sum + a.photoCount, 0).toLocaleString()}
|
{archives.reduce((sum, a) => sum + (a.photoCount || 0), 0).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FileArchive className="w-8 h-8 text-green-600" />
|
<FileArchive className="w-8 h-8 text-green-600" />
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Edit,
|
Edit,
|
||||||
Download,
|
Download,
|
||||||
Trash2
|
Trash2,
|
||||||
|
Calendar,
|
||||||
|
Users,
|
||||||
|
Image,
|
||||||
|
Activity
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { parseISO, differenceInDays } from 'date-fns';
|
import { parseISO, differenceInDays } from 'date-fns';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -232,6 +236,59 @@ export const EventsListPage: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<Card padding="sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-neutral-600">{t('events.stats.totalEvents')}</p>
|
||||||
|
<p className="text-2xl font-bold text-neutral-900">{data?.events.length || 0}</p>
|
||||||
|
</div>
|
||||||
|
<Calendar className="w-8 h-8 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card padding="sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-neutral-600">{t('events.stats.activeEvents')}</p>
|
||||||
|
<p className="text-2xl font-bold text-neutral-900">
|
||||||
|
{data?.events.filter(e => e.is_active && !e.is_archived).length || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Activity className="w-8 h-8 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card padding="sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-neutral-600">{t('events.stats.totalPhotos')}</p>
|
||||||
|
<p className="text-2xl font-bold text-neutral-900">
|
||||||
|
{data?.events.reduce((sum, e) => sum + (e.photo_count || 0), 0) || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Image className="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card padding="sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-neutral-600">{t('events.stats.expiringEvents')}</p>
|
||||||
|
<p className="text-2xl font-bold text-neutral-900">
|
||||||
|
{data?.events.filter(e => {
|
||||||
|
if (!e.is_active || e.is_archived) return false;
|
||||||
|
const days = e.expires_at ? differenceInDays(parseISO(e.expires_at), new Date()) : 0;
|
||||||
|
return days <= 7 && days > 0;
|
||||||
|
}).length || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AlertTriangle className="w-8 h-8 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Filters and Search */}
|
{/* Filters and Search */}
|
||||||
<Card padding="sm" className="mb-6">
|
<Card padding="sm" className="mb-6">
|
||||||
<div className="flex flex-col lg:flex-row gap-4">
|
<div className="flex flex-col lg:flex-row gap-4">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"verbatimModuleSyntax": false,
|
"verbatimModuleSyntax": false,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user