Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66940c2f5b | |||
| a3638fe954 |
@@ -0,0 +1,98 @@
|
|||||||
|
# Production Deployment Guide
|
||||||
|
|
||||||
|
This guide explains how to deploy PicPeak in production behind a reverse proxy like Traefik.
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
### Frontend Configuration
|
||||||
|
|
||||||
|
For production deployment behind a reverse proxy (Traefik, Nginx, etc.), the frontend should use relative URLs to automatically inherit the protocol (HTTPS) and domain.
|
||||||
|
|
||||||
|
1. Copy the production environment template:
|
||||||
|
```bash
|
||||||
|
cp frontend/.env.production.example frontend/.env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set the API URL to use relative path:
|
||||||
|
```env
|
||||||
|
# frontend/.env.production
|
||||||
|
VITE_API_URL=/api
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures all API calls will use the same domain and protocol as the frontend.
|
||||||
|
|
||||||
|
### Backend Configuration
|
||||||
|
|
||||||
|
Ensure your backend `.env` file has the correct URLs:
|
||||||
|
```env
|
||||||
|
# backend/.env
|
||||||
|
FRONTEND_URL=https://yourdomain.com
|
||||||
|
ADMIN_URL=https://yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose Production
|
||||||
|
|
||||||
|
When using Docker Compose in production:
|
||||||
|
|
||||||
|
1. Build with production environment:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml build --build-arg NODE_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The frontend nginx configuration already includes proper proxy settings for:
|
||||||
|
- `/api` → Backend API
|
||||||
|
- `/photos` → Protected photo access
|
||||||
|
- `/thumbnails` → Thumbnail images
|
||||||
|
- `/uploads` → Public uploads (logos, favicons)
|
||||||
|
|
||||||
|
## Traefik Configuration
|
||||||
|
|
||||||
|
Example Traefik labels for docker-compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.picpeak.rule=Host(`yourdomain.com`)"
|
||||||
|
- "traefik.http.routers.picpeak.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.picpeak.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.picpeak.loadbalancer.server.port=80"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **No Hardcoded URLs**: The application uses environment variables with relative URL fallbacks, making it production-ready.
|
||||||
|
|
||||||
|
2. **HTTPS Only**: When `VITE_API_URL=/api`, all requests will use the same protocol as the page (HTTPS in production).
|
||||||
|
|
||||||
|
3. **CORS Configuration**: The backend CORS is configured to accept requests from the URLs specified in `FRONTEND_URL` and `ADMIN_URL`.
|
||||||
|
|
||||||
|
4. **Static Assets**: All static assets (photos, thumbnails, uploads) are served through the nginx proxy, inheriting authentication headers.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After deployment, verify:
|
||||||
|
|
||||||
|
1. Check browser console for any localhost URLs (there should be none)
|
||||||
|
2. Verify all API calls use HTTPS
|
||||||
|
3. Check that images load correctly with authentication
|
||||||
|
4. Test favicon and logo display
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If you see console errors about localhost:
|
||||||
|
|
||||||
|
1. Ensure `VITE_API_URL=/api` in frontend environment
|
||||||
|
2. Clear browser cache
|
||||||
|
3. Rebuild frontend with production environment:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
If images don't load:
|
||||||
|
|
||||||
|
1. Check that nginx proxy locations are configured
|
||||||
|
2. Verify authentication tokens are being sent
|
||||||
|
3. Check backend logs for authentication errors
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"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.2",
|
"version": "1.0.3",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Production Environment Configuration
|
||||||
|
# When running behind a reverse proxy like Traefik, use relative URLs
|
||||||
|
|
||||||
|
# Backend API URL
|
||||||
|
# For production behind reverse proxy, use relative URL:
|
||||||
|
VITE_API_URL=/api
|
||||||
|
|
||||||
|
# For development or if frontend/backend are on different domains:
|
||||||
|
# VITE_API_URL=https://api.yourdomain.com
|
||||||
|
|
||||||
|
# Umami Analytics Configuration (optional)
|
||||||
|
# VITE_UMAMI_URL=https://analytics.yourdomain.com
|
||||||
|
# VITE_UMAMI_WEBSITE_ID=your-website-id-from-umami
|
||||||
|
# VITE_UMAMI_SHARE_URL=https://analytics.yourdomain.com/share/your-share-id/photo-sharing
|
||||||
@@ -79,6 +79,20 @@ server {
|
|||||||
proxy_cache_valid 404 1m;
|
proxy_cache_valid 404 1m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Uploads serving proxy (logos, favicons, watermarks)
|
||||||
|
location /uploads {
|
||||||
|
proxy_pass http://backend:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Cache uploads
|
||||||
|
proxy_cache_valid 200 302 7d;
|
||||||
|
proxy_cache_valid 404 1m;
|
||||||
|
}
|
||||||
|
|
||||||
# SPA fallback
|
# SPA fallback
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"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.2",
|
"version": "1.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { AdminLayout, AdminAuthWrapper } from './components/admin';
|
|||||||
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon } from './components/common';
|
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon } from './components/common';
|
||||||
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
||||||
import { GlobalThemeProvider } from './components/GlobalThemeProvider';
|
import { GlobalThemeProvider } from './components/GlobalThemeProvider';
|
||||||
|
import { getApiBaseUrl } from './utils/url';
|
||||||
|
|
||||||
// Create a client
|
// Create a client
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -48,7 +49,7 @@ function App() {
|
|||||||
if (umamiUrl && umamiWebsiteId) {
|
if (umamiUrl && umamiWebsiteId) {
|
||||||
try {
|
try {
|
||||||
// Fetch public settings to check if analytics is enabled
|
// Fetch public settings to check if analytics is enabled
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||||
const settings = await response.json();
|
const settings = await response.json();
|
||||||
|
|
||||||
// Only initialize if analytics is enabled in settings
|
// Only initialize if analytics is enabled in settings
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AlertTriangle } from 'lucide-react';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { api } from '../config/api';
|
import { api } from '../config/api';
|
||||||
|
import { buildResourceUrl } from '../utils/url';
|
||||||
|
|
||||||
interface BrandingSettings {
|
interface BrandingSettings {
|
||||||
branding_company_name?: string;
|
branding_company_name?: string;
|
||||||
@@ -50,7 +51,7 @@ export const MaintenanceMode: React.FC = () => {
|
|||||||
src={settings?.branding_logo_url ?
|
src={settings?.branding_logo_url ?
|
||||||
(settings.branding_logo_url.startsWith('http')
|
(settings.branding_logo_url.startsWith('http')
|
||||||
? settings.branding_logo_url
|
? settings.branding_logo_url
|
||||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settings.branding_logo_url}`)
|
: buildResourceUrl(settings.branding_logo_url))
|
||||||
: '/picpeak-logo-transparent.png'
|
: '/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={settings?.branding_company_name || 'PicPeak'}
|
alt={settings?.branding_company_name || 'PicPeak'}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button, Card, Input } from '../common';
|
|||||||
import { GALLERY_THEME_PRESETS, type ThemeConfig } from '../../contexts/ThemeContext';
|
import { GALLERY_THEME_PRESETS, type ThemeConfig } from '../../contexts/ThemeContext';
|
||||||
import { settingsService } from '../../services/settings.service';
|
import { settingsService } from '../../services/settings.service';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface ThemeCustomizerProps {
|
interface ThemeCustomizerProps {
|
||||||
value: ThemeConfig;
|
value: ThemeConfig;
|
||||||
@@ -269,7 +270,7 @@ export const ThemeCustomizer: React.FC<ThemeCustomizerProps> = ({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{localTheme.logoUrl && (
|
{localTheme.logoUrl && (
|
||||||
<img
|
<img
|
||||||
src={localTheme.logoUrl.startsWith('http') ? localTheme.logoUrl : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${localTheme.logoUrl}`}
|
src={localTheme.logoUrl.startsWith('http') ? localTheme.logoUrl : buildResourceUrl(localTheme.logoUrl)}
|
||||||
alt="Custom logo"
|
alt="Custom logo"
|
||||||
className="h-16 w-auto object-contain"
|
className="h-16 w-auto object-contain"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { getAuthToken } from '../../config/api';
|
import { getAuthToken } from '../../config/api';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -60,9 +61,8 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
|||||||
// Use the src as-is since it should already be the correct endpoint
|
// Use the src as-is since it should already be the correct endpoint
|
||||||
let imageUrl = src;
|
let imageUrl = src;
|
||||||
|
|
||||||
// Prepend API URL for absolute paths
|
// Build full URL for the image
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
const fullImageUrl = imageUrl.startsWith('/') ? buildResourceUrl(imageUrl) : imageUrl;
|
||||||
const fullImageUrl = imageUrl.startsWith('/') ? `${apiUrl}${imageUrl}` : imageUrl;
|
|
||||||
|
|
||||||
console.log('Fetching authenticated image:', fullImageUrl);
|
console.log('Fetching authenticated image:', fullImageUrl);
|
||||||
const response = await fetch(fullImageUrl, {
|
const response = await fetch(fullImageUrl, {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getApiBaseUrl, buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
export const DynamicFavicon: React.FC = () => {
|
export const DynamicFavicon: React.FC = () => {
|
||||||
const { data: settings } = useQuery({
|
const { data: settings } = useQuery({
|
||||||
queryKey: ['public-settings'],
|
queryKey: ['public-settings'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,7 @@ export const DynamicFavicon: React.FC = () => {
|
|||||||
link.type = 'image/png';
|
link.type = 'image/png';
|
||||||
link.href = settings.branding_favicon_url.startsWith('http')
|
link.href = settings.branding_favicon_url.startsWith('http')
|
||||||
? settings.branding_favicon_url
|
? settings.branding_favicon_url
|
||||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settings.branding_favicon_url}`;
|
: buildResourceUrl(settings.branding_favicon_url);
|
||||||
|
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getApiBaseUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface ReCaptchaProps {
|
interface ReCaptchaProps {
|
||||||
onChange: (token: string | null) => void;
|
onChange: (token: string | null) => void;
|
||||||
@@ -20,7 +21,7 @@ export const ReCaptcha: React.FC<ReCaptchaProps> = ({
|
|||||||
const { data: settings } = useQuery({
|
const { data: settings } = useQuery({
|
||||||
queryKey: ['public-settings'],
|
queryKey: ['public-settings'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
|||||||
import { Button } from '../common';
|
import { Button } from '../common';
|
||||||
import { DynamicFavicon } from '../common/DynamicFavicon';
|
import { DynamicFavicon } from '../common/DynamicFavicon';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface GalleryLayoutProps {
|
interface GalleryLayoutProps {
|
||||||
event: {
|
event: {
|
||||||
@@ -122,7 +123,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={brandingSettings?.logo_url ?
|
src={brandingSettings?.logo_url ?
|
||||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
buildResourceUrl(brandingSettings.logo_url) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||||
@@ -277,7 +278,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<img
|
<img
|
||||||
src={brandingSettings?.logo_url ?
|
src={brandingSettings?.logo_url ?
|
||||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
buildResourceUrl(brandingSettings.logo_url) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTheme } from '../../../contexts/ThemeContext';
|
|||||||
import { AuthenticatedImage } from '../../common';
|
import { AuthenticatedImage } from '../../common';
|
||||||
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
|
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
|
||||||
import type { Photo } from '../../../types';
|
import type { Photo } from '../../../types';
|
||||||
|
import { buildResourceUrl } from '../../../utils/url';
|
||||||
|
|
||||||
interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
||||||
eventName?: string;
|
eventName?: string;
|
||||||
@@ -97,7 +98,7 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<img
|
<img
|
||||||
src={eventLogo ?
|
src={eventLogo ?
|
||||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${eventLogo}` :
|
buildResourceUrl(eventLogo) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt="Event logo"
|
alt="Event logo"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const setMaintenanceModeCallback = (callback: (enabled: boolean) => void)
|
|||||||
|
|
||||||
// Create axios instance
|
// Create axios instance
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001',
|
baseURL: import.meta.env.VITE_API_URL || '/api',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { setMaintenanceModeCallback } from '../config/api';
|
import { setMaintenanceModeCallback } from '../config/api';
|
||||||
|
import { getApiBaseUrl } from '../utils/url';
|
||||||
|
|
||||||
interface MaintenanceContextType {
|
interface MaintenanceContextType {
|
||||||
isMaintenanceMode: boolean;
|
isMaintenanceMode: boolean;
|
||||||
@@ -29,7 +30,7 @@ export const MaintenanceProvider: React.FC<MaintenanceProviderProps> = ({ childr
|
|||||||
queryKey: ['public-settings-maintenance'],
|
queryKey: ['public-settings-maintenance'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||||
if (response.status === 503) {
|
if (response.status === 503) {
|
||||||
setIsMaintenanceMode(true);
|
setIsMaintenanceMode(true);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { GalleryView } from '../components/gallery';
|
|||||||
import { analyticsService } from '../services/analytics.service';
|
import { analyticsService } from '../services/analytics.service';
|
||||||
import { api } from '../config/api';
|
import { api } from '../config/api';
|
||||||
import { GALLERY_THEME_PRESETS } from '../types/theme.types';
|
import { GALLERY_THEME_PRESETS } from '../types/theme.types';
|
||||||
|
import { buildResourceUrl } from '../utils/url';
|
||||||
|
|
||||||
export const GalleryPage: React.FC = () => {
|
export const GalleryPage: React.FC = () => {
|
||||||
const { slug, token } = useParams<{ slug: string; token?: string }>();
|
const { slug, token } = useParams<{ slug: string; token?: string }>();
|
||||||
@@ -164,7 +165,7 @@ export const GalleryPage: React.FC = () => {
|
|||||||
{settingsData?.branding_logo_url && (
|
{settingsData?.branding_logo_url && (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}`}
|
src={buildResourceUrl(settingsData.branding_logo_url)}
|
||||||
alt={settingsData.branding_company_name || 'Company Logo'}
|
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||||
className="h-16 w-auto object-contain mx-auto"
|
className="h-16 w-auto object-contain mx-auto"
|
||||||
/>
|
/>
|
||||||
@@ -220,7 +221,7 @@ export const GalleryPage: React.FC = () => {
|
|||||||
{settingsData?.branding_logo_url && (
|
{settingsData?.branding_logo_url && (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}`}
|
src={buildResourceUrl(settingsData.branding_logo_url)}
|
||||||
alt={settingsData.branding_company_name || 'Company Logo'}
|
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||||
className="h-16 w-auto object-contain mx-auto"
|
className="h-16 w-auto object-contain mx-auto"
|
||||||
/>
|
/>
|
||||||
@@ -282,7 +283,7 @@ export const GalleryPage: React.FC = () => {
|
|||||||
<div className="text-center mb-4 sm:mb-6">
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
<img
|
<img
|
||||||
src={settingsData?.branding_logo_url ?
|
src={settingsData?.branding_logo_url ?
|
||||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}` :
|
buildResourceUrl(settingsData.branding_logo_url) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={settingsData?.branding_company_name || 'PicPeak'}
|
alt={settingsData?.branding_company_name || 'PicPeak'}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTheme, type ThemeConfig, GALLERY_THEME_PRESETS } from '../../context
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { settingsService, type BrandingSettings } from '../../services/settings.service';
|
import { settingsService, type BrandingSettings } from '../../services/settings.service';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
export const BrandingPage: React.FC = () => {
|
export const BrandingPage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -279,7 +280,7 @@ export const BrandingPage: React.FC = () => {
|
|||||||
{brandingSettings.favicon_url && (
|
{brandingSettings.favicon_url && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img
|
<img
|
||||||
src={brandingSettings.favicon_url.startsWith('http') ? brandingSettings.favicon_url : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.favicon_url}`}
|
src={brandingSettings.favicon_url.startsWith('http') ? brandingSettings.favicon_url : buildResourceUrl(brandingSettings.favicon_url)}
|
||||||
alt="Current favicon"
|
alt="Current favicon"
|
||||||
className="w-8 h-8"
|
className="w-8 h-8"
|
||||||
/>
|
/>
|
||||||
@@ -344,7 +345,7 @@ export const BrandingPage: React.FC = () => {
|
|||||||
{brandingSettings.watermark_logo_url && (
|
{brandingSettings.watermark_logo_url && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img
|
<img
|
||||||
src={brandingSettings.watermark_logo_url.startsWith('http') ? brandingSettings.watermark_logo_url : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.watermark_logo_url}`}
|
src={brandingSettings.watermark_logo_url.startsWith('http') ? brandingSettings.watermark_logo_url : buildResourceUrl(brandingSettings.watermark_logo_url)}
|
||||||
alt="Current watermark"
|
alt="Current watermark"
|
||||||
className="h-16 w-auto object-contain bg-neutral-100 p-2 rounded"
|
className="h-16 w-auto object-contain bg-neutral-100 p-2 rounded"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for URL handling in production environments
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base API URL, preferring relative URLs for production
|
||||||
|
* @returns The API base URL
|
||||||
|
*/
|
||||||
|
export const getApiBaseUrl = (): string => {
|
||||||
|
// If VITE_API_URL is explicitly set, use it
|
||||||
|
if (import.meta.env.VITE_API_URL && import.meta.env.VITE_API_URL !== '/api') {
|
||||||
|
return import.meta.env.VITE_API_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In production, use relative URL
|
||||||
|
return '/api';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a full URL for resources (images, files, etc.)
|
||||||
|
* In production, this will use the current origin
|
||||||
|
* @param path - The resource path
|
||||||
|
* @returns The full URL
|
||||||
|
*/
|
||||||
|
export const buildResourceUrl = (path: string): string => {
|
||||||
|
// Remove leading slash if present
|
||||||
|
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||||
|
|
||||||
|
// If we have an explicit API URL that's not relative, use it
|
||||||
|
const apiUrl = import.meta.env.VITE_API_URL;
|
||||||
|
if (apiUrl && apiUrl !== '/api' && apiUrl.startsWith('http')) {
|
||||||
|
const baseUrl = apiUrl.replace(/\/api\/?$/, ''); // Remove /api suffix if present
|
||||||
|
return `${baseUrl}/${cleanPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In production (relative API), use current origin
|
||||||
|
return `${window.location.origin}/${cleanPath}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're in production mode (using relative URLs)
|
||||||
|
* @returns True if in production mode
|
||||||
|
*/
|
||||||
|
export const isProductionMode = (): boolean => {
|
||||||
|
return !import.meta.env.VITE_API_URL || import.meta.env.VITE_API_URL === '/api';
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user