feat: Add photo galleries to services, resource types management, and UI improvements
Major features: - Add drag-and-drop photo gallery to Service create/edit modals - Add Resource Types management section to Settings (CRUD for custom types) - Add edit icon consistency to Resources table (pencil icon in actions) - Improve Services page with drag-to-reorder and customer preview mockup Backend changes: - Add photos JSONField to Service model with migration - Add ResourceType model with category (STAFF/OTHER), description fields - Add ResourceTypeViewSet with CRUD operations - Add service reorder endpoint for display order Frontend changes: - Services page: two-column layout, drag-reorder, photo upload - Settings page: Resource Types tab with full CRUD modal - Resources page: Edit icon in actions column instead of row click - Sidebar: Payments link visibility based on role and paymentsEnabled - Update types.ts with Service.photos and ResourceTypeDefinition Note: Removed photos from ResourceType (kept only for Service) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
? location.pathname === path
|
||||
: location.pathname.startsWith(path);
|
||||
|
||||
const baseClasses = `flex items-center gap-3 py-3 text-sm font-medium rounded-lg transition-colors`;
|
||||
const baseClasses = `flex items-center gap-3 py-3 text-base font-medium rounded-lg transition-colors`;
|
||||
const collapsedClasses = isCollapsed ? 'px-3 justify-center' : 'px-4';
|
||||
const activeClasses = 'bg-white/10 text-white';
|
||||
const inactiveClasses = 'text-white/70 hover:text-white hover:bg-white/5';
|
||||
@@ -70,14 +70,40 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
className={`flex items-center gap-3 w-full text-left px-6 py-8 ${isCollapsed ? 'justify-center' : ''} hover:bg-white/5 transition-colors focus:outline-none`}
|
||||
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
>
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-white rounded-lg text-brand-600 font-bold text-xl shrink-0" style={{ color: business.primaryColor }}>
|
||||
{business.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className="overflow-hidden">
|
||||
<h1 className="font-bold leading-tight truncate">{business.name}</h1>
|
||||
<p className="text-xs text-white/60 truncate">{business.subdomain}.smoothschedule.com</p>
|
||||
{/* Logo-only mode: full width */}
|
||||
{business.logoDisplayMode === 'logo-only' && business.logoUrl ? (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<img
|
||||
src={business.logoUrl}
|
||||
alt={business.name}
|
||||
className="max-w-full max-h-16 object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Logo/Icon display */}
|
||||
{business.logoUrl && business.logoDisplayMode !== 'text-only' ? (
|
||||
<div className="flex items-center justify-center w-10 h-10 shrink-0">
|
||||
<img
|
||||
src={business.logoUrl}
|
||||
alt={business.name}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : business.logoDisplayMode !== 'logo-only' && (
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-white rounded-lg text-brand-600 font-bold text-xl shrink-0" style={{ color: business.primaryColor }}>
|
||||
{business.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text display */}
|
||||
{!isCollapsed && business.logoDisplayMode !== 'logo-only' && (
|
||||
<div className="overflow-hidden">
|
||||
<h1 className="font-bold leading-tight truncate">{business.name}</h1>
|
||||
<p className="text-xs text-white/60 truncate">{business.subdomain}.smoothschedule.com</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -111,19 +137,22 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
|
||||
{canViewAdminPages && (
|
||||
<>
|
||||
{business.paymentsEnabled ? (
|
||||
<Link to="/payments" className={getNavClass('/payments')} title={t('nav.payments')}>
|
||||
<CreditCard size={20} className="shrink-0" />
|
||||
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
className={getNavClass('/payments', false, true)}
|
||||
title={t('nav.paymentsDisabledTooltip')}
|
||||
>
|
||||
<CreditCard size={20} className="shrink-0" />
|
||||
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
||||
</div>
|
||||
{/* Payments link: always visible for owners, only visible for others if enabled */}
|
||||
{(role === 'owner' || business.paymentsEnabled) && (
|
||||
business.paymentsEnabled ? (
|
||||
<Link to="/payments" className={getNavClass('/payments')} title={t('nav.payments')}>
|
||||
<CreditCard size={20} className="shrink-0" />
|
||||
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
className={getNavClass('/payments', false, true)}
|
||||
title={t('nav.paymentsDisabledTooltip')}
|
||||
>
|
||||
<CreditCard size={20} className="shrink-0" />
|
||||
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<Link to="/messages" className={getNavClass('/messages')} title={t('nav.messages')}>
|
||||
<MessageSquare size={20} className="shrink-0" />
|
||||
@@ -149,7 +178,12 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<div className={`flex items-center gap-2 text-xs text-white/60 mb-4 ${isCollapsed ? 'justify-center' : ''}`}>
|
||||
<a
|
||||
href={`${window.location.protocol}//${window.location.host.split('.').slice(-2).join('.')}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex items-center gap-2 text-xs text-white/60 mb-4 hover:text-white/80 transition-colors ${isCollapsed ? 'justify-center' : ''}`}
|
||||
>
|
||||
<SmoothScheduleLogo className="w-6 h-6 text-white" />
|
||||
{!isCollapsed && (
|
||||
<div>
|
||||
@@ -157,7 +191,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
<span className="font-semibold text-white/80">Smooth Schedule</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
disabled={logoutMutation.isPending}
|
||||
|
||||
@@ -4,13 +4,15 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Menu, X, Sun, Moon } from 'lucide-react';
|
||||
import SmoothScheduleLogo from '../SmoothScheduleLogo';
|
||||
import LanguageSelector from '../LanguageSelector';
|
||||
import { User } from '../../api/auth';
|
||||
|
||||
interface NavbarProps {
|
||||
darkMode: boolean;
|
||||
toggleTheme: () => void;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ darkMode, toggleTheme }) => {
|
||||
const Navbar: React.FC<NavbarProps> = ({ darkMode, toggleTheme, user }) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -38,6 +40,21 @@ const Navbar: React.FC<NavbarProps> = ({ darkMode, toggleTheme }) => {
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
// Get the dashboard URL based on user role
|
||||
const getDashboardUrl = (): string => {
|
||||
if (!user) return '/login';
|
||||
const port = window.location.port ? `:${window.location.port}` : '';
|
||||
const protocol = window.location.protocol;
|
||||
|
||||
if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) {
|
||||
return `${protocol}//platform.lvh.me${port}/`;
|
||||
}
|
||||
if (user.business_subdomain) {
|
||||
return `${protocol}//${user.business_subdomain}.lvh.me${port}/`;
|
||||
}
|
||||
return '/login';
|
||||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
@@ -90,12 +107,21 @@ const Navbar: React.FC<NavbarProps> = ({ darkMode, toggleTheme }) => {
|
||||
</button>
|
||||
|
||||
{/* Login Button - Hidden on mobile */}
|
||||
<Link
|
||||
to="/login"
|
||||
className="hidden md:inline-flex px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-brand-600 dark:hover:text-brand-400 transition-colors"
|
||||
>
|
||||
{t('marketing.nav.login')}
|
||||
</Link>
|
||||
{user ? (
|
||||
<a
|
||||
href={getDashboardUrl()}
|
||||
className="hidden md:inline-flex px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-brand-600 dark:hover:text-brand-400 transition-colors"
|
||||
>
|
||||
{t('marketing.nav.login')}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="hidden md:inline-flex px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-brand-600 dark:hover:text-brand-400 transition-colors"
|
||||
>
|
||||
{t('marketing.nav.login')}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Get Started CTA */}
|
||||
<Link
|
||||
@@ -139,12 +165,21 @@ const Navbar: React.FC<NavbarProps> = ({ darkMode, toggleTheme }) => {
|
||||
</Link>
|
||||
))}
|
||||
<hr className="my-2 border-gray-200 dark:border-gray-800" />
|
||||
<Link
|
||||
to="/login"
|
||||
className="px-4 py-3 rounded-lg text-sm font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
{t('marketing.nav.login')}
|
||||
</Link>
|
||||
{user ? (
|
||||
<a
|
||||
href={getDashboardUrl()}
|
||||
className="px-4 py-3 rounded-lg text-sm font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
{t('marketing.nav.login')}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="px-4 py-3 rounded-lg text-sm font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
{t('marketing.nav.login')}
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to="/signup"
|
||||
className="px-4 py-3 rounded-lg text-sm font-medium text-center text-white bg-brand-600 hover:bg-brand-700 transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user