feat: add admin dark mode and SEO/robots.txt settings
Admin Dark Mode: - Add AdminDarkModeContext with light/dark/system preference - Update all admin components with Tailwind dark: classes - Add dark mode toggle in admin header - Persist preference in localStorage SEO Settings: - Add robots.txt configuration in Settings > SEO tab - Block AI crawlers (GPTBot, ChatGPT-User, etc.) with toggle - Custom robots.txt rules management - Add RobotsMetaTags component for gallery pages - Backend service for dynamic robots.txt generation - Database migration for SEO settings storage UI/UX Improvements: - Consistent dark mode styling across all admin pages - Update gallery components with themed CSS classes - Fix input, card, and button styling for dark mode
This commit is contained in:
@@ -30,7 +30,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="block text-sm font-medium text-neutral-700 mb-1.5"
|
||||
className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1.5"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
@@ -38,7 +38,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<div className="relative">
|
||||
{leftIcon && (
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span className="text-neutral-500">{leftIcon}</span>
|
||||
<span className="text-neutral-500 dark:text-neutral-400">{leftIcon}</span>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
@@ -59,17 +59,17 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
/>
|
||||
{rightIcon && (
|
||||
<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 dark:text-neutral-400">{rightIcon}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p id={`${inputId}-error`} className="mt-1.5 text-sm text-red-600">
|
||||
<p id={`${inputId}-error`} className="mt-1.5 text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{helperText && !error && (
|
||||
<p id={`${inputId}-helper`} className="mt-1.5 text-sm text-neutral-500">
|
||||
<p id={`${inputId}-helper`} className="mt-1.5 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const LanguageSelector: React.FC = () => {
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-neutral-700 bg-white border border-neutral-300 rounded-lg hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<Globe className="w-4 h-4" />
|
||||
<currentLanguage.Flag className="w-5 h-5" />
|
||||
@@ -49,15 +49,15 @@ export const LanguageSelector: React.FC = () => {
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-neutral-200 py-1 z-50">
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 py-1 z-50">
|
||||
{languages.map((language) => (
|
||||
<button
|
||||
key={language.code}
|
||||
onClick={() => handleLanguageChange(language.code)}
|
||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-neutral-50 flex items-center gap-3 ${
|
||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-neutral-50 dark:hover:bg-neutral-700 flex items-center gap-3 ${
|
||||
language.code === i18n.language
|
||||
? 'text-primary-600 bg-primary-50'
|
||||
: 'text-neutral-700'
|
||||
? 'text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/30'
|
||||
: 'text-neutral-700 dark:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
<language.Flag className="w-5 h-5" />
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl } from '../../utils/url';
|
||||
|
||||
export const RobotsMetaTags: React.FC = () => {
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Remove any existing robots meta tags we previously injected
|
||||
document.querySelectorAll('meta[name="robots"][data-picpeak]').forEach(el => el.remove());
|
||||
|
||||
const directives: string[] = [];
|
||||
if (settings?.seo_meta_noindex) directives.push('noindex');
|
||||
if (settings?.seo_meta_nofollow) directives.push('nofollow');
|
||||
|
||||
if (directives.length > 0) {
|
||||
const meta = document.createElement('meta');
|
||||
meta.name = 'robots';
|
||||
meta.content = directives.join(', ');
|
||||
meta.setAttribute('data-picpeak', 'true');
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
|
||||
if (settings?.seo_meta_noai) {
|
||||
const metaAi = document.createElement('meta');
|
||||
metaAi.name = 'robots';
|
||||
metaAi.content = 'noai, noimageai';
|
||||
metaAi.setAttribute('data-picpeak', 'true');
|
||||
document.head.appendChild(metaAi);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.querySelectorAll('meta[name="robots"][data-picpeak]').forEach(el => el.remove());
|
||||
};
|
||||
}, [settings?.seo_meta_noindex, settings?.seo_meta_nofollow, settings?.seo_meta_noai]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
export { OfflineIndicator, useOnlineStatus } from './OfflineIndicator';
|
||||
export { SkipLink } from './SkipLink';
|
||||
export { DynamicFavicon } from './DynamicFavicon';
|
||||
export { RobotsMetaTags } from './RobotsMetaTags';
|
||||
export { LanguageSelector } from './LanguageSelector';
|
||||
export { AuthenticatedImage } from './AuthenticatedImage';
|
||||
export { AuthenticatedVideo } from './AuthenticatedVideo';
|
||||
|
||||
Reference in New Issue
Block a user