S SmartDocs
系列: Golden-Hour-backend-layer-2 typescript 281 行 · 更新于 2026-02-22

Dashboard.tsx

Golden-Hour-backend-layer-2/pages/Dashboard.tsx


import React, { useState, useMemo } from 'react';

type ViewMode = 'Day' | 'Week' | 'Month';

interface CalendarEvent {
  id: string;
  title: string;
  time: string;
  date: Date;
  category: 'Maisons' | 'Experiences' | 'Services' | 'Products';
  client: string;
  description?: string;
}

const Dashboard: React.FC = () => {
  const [viewMode, setViewMode] = useState<ViewMode>('Month');
  const [currentDate, setCurrentDate] = useState(new Date());

  const events: CalendarEvent[] = useMemo(() => [
    { id: '1', title: 'Villa Maintenance', time: '09:00', date: new Date(2025, 4, 15), category: 'Maisons', client: 'Alice Smith', description: 'Routine check of the HVAC and pool systems.' },
    { id: '2', title: 'Mountain Hike', time: '10:30', date: new Date(2025, 4, 16), category: 'Experiences', client: 'Bob Chen', description: 'Guided tour through the alpine trail.' },
    { id: '3', title: 'Private Chef', time: '19:00', date: new Date(2025, 4, 15), category: 'Services', client: 'Sarah Miller', description: 'Mediterranean 5-course dinner preparation.' },
    { id: '4', title: 'Cleaning Service', time: '14:00', date: new Date(2025, 4, 18), category: 'Maisons', client: 'John Doe', description: 'Full property turnover.' },
    { id: '5', title: 'City Tour', time: '11:00', date: new Date(2025, 4, 20), category: 'Experiences', client: 'Emma Watson', description: 'Art-deco architecture walking tour.' },
    { id: '6', title: 'Product Delivery', time: '16:00', date: new Date(2025, 4, 15), category: 'Products', client: 'Mark Ross', description: 'Luxury hamper delivery.' },
  ], []);

  const getDaysInMonth = (date: Date) => {
    const year = date.getFullYear();
    const month = date.getMonth();
    const firstDay = new Date(year, month, 1).getDay();
    const lastDate = new Date(year, month + 1, 0).getDate();
    let days: (Date | null)[] = [];
    for (let i = 0; i < firstDay; i++) days.push(null);
    for (let i = 1; i <= lastDate; i++) days.push(new Date(year, month, i));
    const totalCells = 35; 
    while (days.length < totalCells) days.push(null);
    return days;
  };

  const getDaysInWeek = (date: Date) => {
    const days = [];
    const current = new Date(date);
    const day = current.getDay();
    const diff = current.getDate() - day;
    const startOfWeek = new Date(current.setDate(diff));
    for (let i = 0; i < 7; i++) {
      days.push(new Date(startOfWeek));
      startOfWeek.setDate(startOfWeek.getDate() + 1);
    }
    return days;
  };

  const navigateDate = (direction: number) => {
    const newDate = new Date(currentDate);
    if (viewMode === 'Month') newDate.setMonth(currentDate.getMonth() + direction);
    else if (viewMode === 'Week') newDate.setDate(currentDate.getDate() + (direction * 7));
    else newDate.setDate(currentDate.getDate() + direction);
    setCurrentDate(newDate);
  };

  const isToday = (date: Date | null) => {
    if (!date) return false;
    const today = new Date();
    return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
  };

  const getEventsForDate = (date: Date | null) => {
    if (!date) return [];
    return events.filter(e => e.date.getDate() === date.getDate() && e.date.getMonth() === date.getMonth());
  };

  const categoryColors: Record<string, string> = {
    Maisons: 'bg-[#c5a059]',
    Experiences: 'bg-stone-300',
    Services: 'bg-stone-200',
    Products: 'bg-stone-800'
  };

  const renderMonthView = () => (
    <div className="flex-1 bg-white gallery-shadow rounded-xl overflow-hidden border border-[#c5a059]/10 flex flex-col fade-in-section">
      <div className="grid grid-cols-7 border-b border-[#c5a059]/5 bg-[#fdfcf9]/50">
        {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(day => (
          <div key={day} className="py-2.5 text-center text-[7px] font-bold uppercase tracking-[0.3em] text-slate-300 font-serif italic">
            {day}
          </div>
        ))}
      </div>
      
      <div className="grid grid-cols-7 grid-rows-5 flex-1 divide-x divide-y divide-[#c5a059]/5">
        {getDaysInMonth(currentDate).map((date, idx) => {
          const dayEvents = getEventsForDate(date);
          return (
            <div 
              key={idx} 
              className={`min-h-[85px] p-3 relative group transition-all duration-700 hover:bg-[#fdfcf9] ${!date ? 'bg-[#f4f2ee]/10' : ''}`}
            >
              {date ? (
                <>
                  <div className="flex justify-between items-start mb-1.5">
                    <span className={`serif-numeral text-sm leading-none transition-all duration-700 ${
                      isToday(date) ? 'text-[#c5a059] font-medium' : 'text-stone-200 group-hover:text-slate-800'
                    }`}>
                      {date.getDate()}
                    </span>
                  </div>
                  <div className="space-y-1">
                    {dayEvents.slice(0, 2).map(event => (
                      <div key={event.id} className="flex items-center space-x-1.5">
                        <div className={`w-0.5 h-0.5 rounded-full ${categoryColors[event.category]} opacity-40`}></div>
                        <p className="text-[7px] font-bold text-slate-600 tracking-tight truncate uppercase leading-none">{event.title}</p>
                      </div>
                    ))}
                  </div>
                </>
              ) : null}
            </div>
          );
        })}
      </div>
    </div>
  );

  const renderWeekView = () => (
    <div className="flex-1 grid grid-cols-7 gap-4 fade-in-section">
      {getDaysInWeek(currentDate).map((date, idx) => {
        const dayEvents = getEventsForDate(date);
        return (
          <div key={idx} className={`bg-white gallery-shadow rounded-lg border border-[#c5a059]/10 p-5 flex flex-col transition-all duration-700 ${isToday(date) ? 'ring-1 ring-[#c5a059]/10 shadow-sm shadow-[#c5a059]/5' : ''}`}>
            <div className="text-center mb-4 pb-3 border-b border-[#c5a059]/5">
              <p className="text-[7px] font-bold text-[#c5a059]/50 uppercase tracking-[0.2em] mb-1 font-serif italic">
                {date.toLocaleDateString('en-US', { weekday: 'short' })}
              </p>
              <h4 className={`serif-numeral text-2xl ${isToday(date) ? 'text-slate-900' : 'text-stone-100'}`}>
                {date.getDate()}
              </h4>
            </div>
            <div className="flex-1 space-y-4 overflow-y-auto no-scrollbar">
              {dayEvents.map(event => (
                <div key={event.id} className="group cursor-default border-b border-transparent hover:border-[#c5a059]/10 pb-2 transition-all">
                  <p className="text-[6.5px] font-bold uppercase tracking-[0.1em] text-[#c5a059]/30 font-serif italic">{event.time}</p>
                  <h5 className="text-[8px] font-bold text-slate-800 uppercase tracking-widest mt-0.5 leading-tight group-hover:text-[#c5a059] transition-all">{event.title}</h5>
                  <p className="text-[7.5px] font-serif italic text-slate-400">{event.client}</p>
                </div>
              ))}
            </div>
          </div>
        );
      })}
    </div>
  );

  const renderDayView = () => {
    const dayEvents = getEventsForDate(currentDate);
    return (
      <div className="flex-1 flex gap-8 fade-in-section">
        <div className="flex-1 bg-white gallery-shadow rounded-2xl p-10 border border-[#c5a059]/10 flex flex-col">
          <div className="flex items-center justify-between mb-10 border-b border-[#c5a059]/5 pb-8">
            <div>
              <p className="text-[7px] font-bold uppercase tracking-[0.3em] text-[#c5a059] mb-1 font-serif italic">Edition Quotidienne</p>
              <h3 className="font-serif italic text-4xl text-slate-900 leading-none">
                {currentDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
              </h3>
            </div>
            <div className="flex -space-x-3">
              {[1, 2].map(i => (
                <div key={i} className="w-9 h-9 rounded-full border border-white grayscale bg-stone-50 overflow-hidden shadow-sm">
                  <img src={`https://picsum.photos/seed/day-${i}/80/80`} alt="" />
                </div>
              ))}
            </div>
          </div>

          <div className="flex-1 overflow-y-auto pr-4 space-y-8 no-scrollbar">
            {dayEvents.length > 0 ? dayEvents.map(event => (
              <div key={event.id} className="flex group">
                <div className="w-20 flex-shrink-0 pt-0.5">
                  <p className="serif-numeral text-lg text-stone-200 group-hover:text-[#c5a059] transition-colors">{event.time}</p>
                  <div className="w-[0.5px] h-full bg-[#c5a059]/5 mx-auto mt-3"></div>
                </div>
                <div className="flex-1 bg-[#fdfcf9]/30 rounded-xl p-5 border border-[#c5a059]/5 group-hover:border-[#c5a059]/10 transition-all duration-500">
                  <span className="text-[7px] font-bold uppercase tracking-[0.2em] text-[#c5a059]/60 italic font-serif leading-none">{event.category}</span>
                  <h4 className="font-serif italic text-2xl text-slate-900 mt-1.5 mb-1">{event.title}</h4>
                  <p className="text-[11px] text-slate-500 font-serif italic leading-snug">{event.description}</p>
                </div>
              </div>
            )) : (
              <div className="flex-1 flex items-center justify-center text-slate-200 font-serif italic text-2xl opacity-40">
                Aucun événement prévu pour aujourd'hui
              </div>
            )}
          </div>
        </div>

        <div className="w-72 space-y-8">
           <div className="bg-[#1a1a1a] rounded-2xl p-8 gallery-shadow text-white relative overflow-hidden">
             <div className="absolute top-0 right-0 w-24 h-24 bg-[#c5a059]/5 rounded-full blur-3xl -mr-12 -mt-12"></div>
             <p className="text-[7px] font-bold uppercase tracking-[0.3em] text-[#c5a059] mb-8 font-serif italic relative z-10">Sommaire</p>
             <div className="space-y-6 relative z-10">
               <div className="flex justify-between items-end border-b border-white/5 pb-3">
                 <span className="text-stone-400 font-serif italic text-[12px]">Registry</span>
                 <span className="serif-numeral text-2xl">{dayEvents.length}</span>
               </div>
               <div className="flex justify-between items-end border-b border-white/5 pb-3">
                 <span className="text-stone-400 font-serif italic text-[12px]">Occupation</span>
                 <span className="serif-numeral text-2xl">84%</span>
               </div>
             </div>
           </div>

           <div className="bg-white rounded-2xl p-8 border border-[#c5a059]/10">
             <h4 className="font-serif italic text-xl text-slate-900 mb-6">Météo</h4>
             <div className="flex items-center space-x-4">
                <div className="w-9 h-9 rounded-full bg-[#fdfcf9] flex items-center justify-center text-[#c5a059]">
                  <i className="fa-solid fa-sun text-sm"></i>
                </div>
                <div>
                   <p className="text-[6.5px] font-bold uppercase tracking-[0.1em] text-slate-300 font-serif italic">Ciel Dégagé</p>
                   <p className="text-[15px] font-serif italic text-slate-900 leading-none">Antibes, FR</p>
                </div>
             </div>
           </div>
        </div>
      </div>
    );
  };

  return (
    <div className="fade-in-section flex flex-col h-full space-y-8 pb-10">
      <div className="flex flex-col md:flex-row md:items-end justify-between gap-6 border-b border-[#c5a059]/10 pb-8">
        <div className="flex items-baseline space-x-4">
          <span className="font-serif italic text-5xl text-stone-100 select-none">Nº</span>
          <div>
            <h2 className={`font-serif italic text-slate-900 tracking-tighter transition-all duration-700 leading-none ${viewMode === 'Day' ? 'text-6xl' : 'text-4xl'}`}>
              {viewMode === 'Day' 
                ? currentDate.getDate() 
                : currentDate.toLocaleString('default', { month: 'long' })}
            </h2>
            <p className="text-[7px] font-bold uppercase tracking-[0.4em] text-[#c5a059] mt-2 leading-none font-serif italic">
              Catalogue Index — {currentDate.getFullYear()}
            </p>
          </div>
        </div>

        <div className="flex items-center space-x-10">
          <div className="flex items-center space-x-4">
             <button onClick={() => navigateDate(-1)} className="text-[#c5a059]/60 hover:text-[#c5a059] transition-all p-1">
                <i className="fa-solid fa-arrow-left-long text-[9px]"></i>
             </button>
             <button onClick={() => setCurrentDate(new Date())} className="text-[7.5px] font-bold uppercase tracking-[0.2em] text-stone-300 hover:text-slate-900 font-serif italic">Aujourd'hui</button>
             <button onClick={() => navigateDate(1)} className="text-[#c5a059]/60 hover:text-[#c5a059] transition-all p-1">
                <i className="fa-solid fa-arrow-right-long text-[9px]"></i>
             </button>
          </div>
          <div className="h-6 w-[0.5px] bg-[#c5a059]/10"></div>
          <div className="flex space-x-6">
            {(['Day', 'Week', 'Month'] as ViewMode[]).map((mode) => (
              <button
                key={mode}
                onClick={() => setViewMode(mode)}
                className={`text-[7.5px] font-bold uppercase tracking-[0.2em] transition-all relative pb-1.5 font-serif italic ${
                  viewMode === mode ? 'text-slate-900' : 'text-stone-300 hover:text-slate-500'
                }`}
              >
                {mode}
                {viewMode === mode && <div className="absolute bottom-0 left-0 right-0 h-[1px] bg-[#c5a059]"></div>}
              </button>
            ))}
          </div>
        </div>
      </div>

      {viewMode === 'Month' && renderMonthView()}
      {viewMode === 'Week' && renderWeekView()}
      {viewMode === 'Day' && renderDayView()}
    </div>
  );
};

export default Dashboard;

相关文章