Bläddra i källkod

style: применён дизайн-система Twilight

- Обновлён tailwind.config.js с кастомными цветами (primary, secondary, accent, neutral)
- Добавлены CSS-переменные для light/dark тем в globals.css
- Обновлён Header.tsx: sticky навигация, новые цвета, анимации
- Обновлён LoginModal.tsx: новые стили, анимации, лейблы
- Обновлён RegisterModal.tsx: новые стили, лейблы, структура
- Обновлён AddLocationModal.tsx: анимации, новые цвета
- Обновлён PlaceForm.tsx: лейблы, новые цвета, переходы
- Обновлён MapView.tsx: новые цвета, анимации
- Обновлён MapPicker.tsx: новые цвета, шрифт для координат
neyrogovnarik 1 månad sedan
förälder
incheckning
ad4a5ca776

+ 3 - 3
frontend/src/components/AddLocationModal.tsx

@@ -4,12 +4,12 @@ import PlaceForm from './PlaceForm'
 
 export default function AddLocationModal({ onClose }: { onClose: () => void }) {
   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
-      <div className="w-full max-w-2xl overflow-hidden rounded-2xl bg-[#1e293b]">
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in">
+      <div className="w-full max-w-2xl overflow-hidden rounded-xl bg-neutral-900 border border-neutral-800 shadow-xl animate-scale-in">
         <div className="relative max-h-[90vh] overflow-y-auto">
           <button
             onClick={onClose}
-            className="absolute right-4 top-4 z-10 text-white/60 hover:text-white"
+            className="absolute right-4 top-4 z-10 rounded-lg p-2 text-neutral-400 hover:bg-neutral-800 hover:text-neutral-200 transition-colors"
           >
           </button>

+ 66 - 43
frontend/src/components/Header.tsx

@@ -19,52 +19,75 @@ export default function Header() {
       {showRegister && <RegisterModal onClose={() => setShowRegister(false)} />}
       {showAddLocation && <AddLocationModal onClose={() => setShowAddLocation(false)} />}
 
-      <header className="absolute left-0 right-0 top-0 z-30 flex items-center justify-between px-4 py-3">
-        <Link href="/" className="text-lg font-bold text-white">
-          PhotoPlaces
-        </Link>
-
-        {isLoading ? null : user ? (
-          <div className="inline-flex items-center gap-3 rounded-lg bg-white/10 backdrop-blur-md px-4 py-2">
-            <button
-              onClick={() => setShowAddLocation(true)}
-              className="rounded-lg bg-white/10 backdrop-blur-md px-3 py-1 text-sm font-bold text-white hover:bg-white/20 transition"
-              title="Добавить локацию"
-            >
-              +
-            </button>
-            <Link
-              href="/places/my"
-              className="text-sm text-white hover:text-green-400 transition"
-            >
-              Мои места
+      <header className="sticky top-0 z-40 w-full bg-neutral-950/80 backdrop-blur-xl border-b border-neutral-800">
+        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+          <div className="flex h-16 items-center justify-between">
+            <Link href="/" className="flex items-center gap-2">
+              <span className="text-xl font-bold text-neutral-100">
+                PhotoPlaces Twilight
+              </span>
             </Link>
-            {(user.role === 'moderator' || user.role === 'superadmin') && (
-              <Link
-                href="/admin"
-                className="text-sm text-green-400 hover:text-green-300 transition"
-              >
-                Админка
-              </Link>
+
+            {isLoading ? null : user ? (
+              <div className="flex items-center gap-4">
+                <nav className="hidden md:flex items-center gap-6">
+                  <Link
+                    href="/places"
+                    className="text-sm text-neutral-400 hover:text-neutral-100 transition-colors"
+                  >
+                    Локации
+                  </Link>
+                  <Link
+                    href="/places/my"
+                    className="text-sm text-neutral-400 hover:text-neutral-100 transition-colors"
+                  >
+                    Мои места
+                  </Link>
+                  {(user.role === 'moderator' || user.role === 'superadmin') && (
+                    <Link
+                      href="/admin"
+                      className="text-sm text-primary-400 hover:text-primary-300 transition-colors"
+                    >
+                      Админка
+                    </Link>
+                  )}
+                </nav>
+
+                <div className="flex items-center gap-3">
+                  <button
+                    onClick={() => setShowAddLocation(true)}
+                    className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-500 transition-colors"
+                    title="Добавить локацию"
+                  >
+                    + Добавить
+                  </button>
+                  <span className="text-sm text-neutral-400">{user.name || user.email}</span>
+                  <button
+                    onClick={logout}
+                    className="rounded-lg bg-neutral-800 px-3 py-2 text-sm text-neutral-400 hover:bg-neutral-700 hover:text-neutral-200 transition-colors"
+                  >
+                    Выйти
+                  </button>
+                </div>
+              </div>
+            ) : (
+              <div className="flex items-center gap-3">
+                <button
+                  onClick={() => setShowLogin(true)}
+                  className="rounded-lg bg-neutral-800 px-4 py-2 text-sm font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors"
+                >
+                  Войти
+                </button>
+                <button
+                  onClick={() => setShowRegister(true)}
+                  className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-500 transition-colors"
+                >
+                  Регистрация
+                </button>
+              </div>
             )}
-            <span className="text-sm text-white/60">{user.name || user.email}</span>
-            <button
-              onClick={logout}
-              className="rounded-lg bg-white/10 backdrop-blur-md px-3 py-1 text-sm text-white/80 hover:bg-[#ff5656] hover:text-white transition-colors"
-            >
-              Выйти
-            </button>
-          </div>
-        ) : (
-          <div className="flex items-center gap-3">
-            <button onClick={() => setShowLogin(true)} className="rounded-lg bg-white/10 backdrop-blur-md px-3 py-1 text-sm text-white hover:bg-white/20 transition">
-              Войти
-            </button>
-            <button onClick={() => setShowRegister(true)} className="rounded-lg bg-green-600/70 backdrop-blur-md px-3 py-1 text-sm text-white hover:bg-green-500 transition">
-              Регистрация
-            </button>
           </div>
-        )}
+        </div>
       </header>
     </>
   )

+ 34 - 24
frontend/src/components/LoginModal.tsx

@@ -20,37 +20,47 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
   }
 
   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
       <form
         onSubmit={handleSubmit}
         onClick={(e) => e.stopPropagation()}
-        className="w-full max-w-sm space-y-4 rounded-2xl bg-white/10 p-8 backdrop-blur-md"
+        className="w-full max-w-sm space-y-4 rounded-xl bg-neutral-900 border border-neutral-800 p-8 shadow-xl animate-scale-in"
       >
-        <h1 className="text-center text-2xl font-bold text-white">Вход</h1>
-
-        {error && <p className="text-sm text-red-400">{error}</p>}
-
-        <input
-          type="email"
-          placeholder="Email"
-          value={email}
-          onChange={(e) => setEmail(e.target.value)}
-          className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/40 outline-none focus:border-green-500"
-          required
-        />
-
-        <input
-          type="password"
-          placeholder="Пароль"
-          value={password}
-          onChange={(e) => setPassword(e.target.value)}
-          className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/40 outline-none focus:border-green-500"
-          required
-        />
+        <h1 className="text-center text-2xl font-bold text-neutral-100">Вход</h1>
+
+        {error && (
+          <div className="rounded-lg bg-error-500/10 p-3 text-sm text-error-500">
+            {error}
+          </div>
+        )}
+
+        <div className="space-y-2">
+          <label className="text-sm font-medium text-neutral-300">Email</label>
+          <input
+            type="email"
+            placeholder="email@example.com"
+            value={email}
+            onChange={(e) => setEmail(e.target.value)}
+            className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
+            required
+          />
+        </div>
+
+        <div className="space-y-2">
+          <label className="text-sm font-medium text-neutral-300">Пароль</label>
+          <input
+            type="password"
+            placeholder="••••••••"
+            value={password}
+            onChange={(e) => setPassword(e.target.value)}
+            className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
+            required
+          />
+        </div>
 
         <button
           type="submit"
-          className="w-full rounded-lg bg-green-600 px-4 py-3 font-medium text-white hover:bg-green-500 transition"
+          className="w-full rounded-lg bg-primary-600 px-4 py-3 font-medium text-white hover:bg-primary-500 transition-colors"
         >
           Войти
         </button>

+ 5 - 5
frontend/src/components/MapPicker.tsx

@@ -69,23 +69,23 @@ export default function MapPicker({ onSelect, onClose }: MapPickerProps) {
   }
 
   return (
-    <div className="fixed inset-0 z-[60] flex flex-col bg-black/80 backdrop-blur-sm">
+    <div className="fixed inset-0 z-[60] flex flex-col bg-neutral-950 animate-fade-in">
       <div ref={mapRef} className="flex-1" />
-      <div className="flex items-center justify-between bg-[#1e293b] px-4 py-3">
-        <div className="text-sm text-white/60">
+      <div className="flex items-center justify-between bg-neutral-900 border-t border-neutral-800 px-4 py-3">
+        <div className="text-sm text-neutral-400 font-mono">
           {lat.toFixed(6)}, {lng.toFixed(6)}
         </div>
         <div className="flex gap-3">
           <button
             onClick={onClose}
-            className="rounded-lg bg-white/10 px-4 py-2 text-sm text-white hover:bg-white/20 transition"
+            className="rounded-lg bg-neutral-800 px-4 py-2 text-sm font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors"
           >
             Отмена
           </button>
           <button
             onClick={handleConfirm}
             disabled={loading}
-            className="rounded-lg bg-green-600 px-4 py-2 text-sm text-white hover:bg-green-500 transition disabled:opacity-50"
+            className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-500 transition-colors disabled:opacity-50"
           >
             {loading ? 'Загрузка...' : 'Подтвердить'}
           </button>

+ 17 - 12
frontend/src/components/MapView.tsx

@@ -89,10 +89,10 @@ export default function MapView() {
   return (
     <div className="relative h-full w-full">
       {mapError ? (
-        <div className="flex h-full w-full items-center justify-center">
+        <div className="flex h-full w-full items-center justify-center bg-neutral-950">
           <div className="text-center">
-            <p className="mb-2 text-lg text-white/60">Карта временно недоступна</p>
-            <p className="text-sm text-white/40">Попробуйте обновить страницу позже</p>
+            <p className="mb-2 text-lg text-neutral-400">Карта временно недоступна</p>
+            <p className="text-sm text-neutral-500">Попробуйте обновить страницу позже</p>
           </div>
         </div>
       ) : (
@@ -100,8 +100,8 @@ export default function MapView() {
       )}
 
       {!user && !isLoading && !mapError && (
-        <div className="absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white/10 px-6 py-3 text-center text-white backdrop-blur-md">
-          <p className="text-sm">Зарегистрируйтесь, чтобы исследовать места для фотосъёмок</p>
+        <div className="absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2 rounded-xl bg-neutral-900/90 backdrop-blur-md border border-neutral-800 px-6 py-3 text-center shadow-lg">
+          <p className="text-sm text-neutral-300">Зарегистрируйтесь, чтобы исследовать места для фотосъёмок</p>
         </div>
       )}
 
@@ -117,17 +117,22 @@ export default function MapView() {
 
 function PlaceCardModal({ place, onClose }: { place: Place; onClose: () => void }) {
   return (
-    <div className="absolute bottom-0 left-0 right-0 z-20 max-h-[60vh] overflow-y-auto rounded-t-2xl bg-[#1e293b] p-6 shadow-xl">
-      <button onClick={onClose} className="mb-4 text-white/60 hover:text-white">✕</button>
-      <h2 className="mb-2 text-xl font-bold text-white">{place.title}</h2>
-      {place.address && <p className="mb-2 text-sm text-white/60">{place.address}</p>}
-      {place.description && <p className="mb-4 text-sm text-white/80">{place.description}</p>}
+    <div className="absolute bottom-0 left-0 right-0 z-20 max-h-[60vh] overflow-y-auto rounded-t-xl bg-neutral-900 border-t border-neutral-800 p-6 shadow-xl animate-slide-up">
+      <button
+        onClick={onClose}
+        className="mb-4 rounded-lg p-2 text-neutral-400 hover:bg-neutral-800 hover:text-neutral-200 transition-colors"
+      >
+        ✕
+      </button>
+      <h2 className="mb-2 text-xl font-bold text-neutral-100">{place.title}</h2>
+      {place.address && <p className="mb-2 text-sm text-neutral-400">{place.address}</p>}
+      {place.description && <p className="mb-4 text-sm text-neutral-300">{place.description}</p>}
       <div className="mb-4 flex flex-wrap gap-2">
         {place.tags?.map((tag) => (
-          <span key={tag.id} className="rounded-full bg-white/10 px-3 py-1 text-xs text-white/80">{tag.name}</span>
+          <span key={tag.id} className="rounded-full bg-primary-500/20 px-3 py-1 text-xs text-primary-400">{tag.name}</span>
         ))}
       </div>
-      <div className="flex items-center gap-4 text-sm text-white/60">
+      <div className="flex items-center gap-4 text-sm text-neutral-400">
         <span>★ {place.rating?.toFixed(1) || 'Нет оценок'}</span>
         {place.type === 'studio' && place.hourly_rate && <span>{place.hourly_rate} ₽/час</span>}
       </div>

+ 48 - 44
frontend/src/components/PlaceForm.tsx

@@ -144,11 +144,11 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
   if (saved) {
     return (
       <div className="mx-auto max-w-2xl space-y-4 p-6 text-center">
-        <p className="text-lg font-medium text-green-400">
+        <p className="text-lg font-medium text-success-500">
           {saved === 'draft' ? 'Черновик сохранён' : 'Место отправлено на модерацию'}
         </p>
         <a href={type === 'studio' ? '/studios/add' : '/places/add'}
-          className="inline-block rounded-lg bg-green-600 px-6 py-2 text-white hover:bg-green-500 transition"
+          className="inline-block rounded-lg bg-primary-600 px-6 py-2 text-white hover:bg-primary-500 transition-colors"
         >Добавить ещё</a>
       </div>
     )
@@ -156,86 +156,90 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
 
   return (
     <form onSubmit={handleSubmit} className="mx-auto max-w-2xl space-y-6 p-6">
-      <h1 className="text-2xl font-bold text-white">
+      <h1 className="text-2xl font-bold text-neutral-100">
         {isEdit ? 'Редактировать' : type === 'studio' ? 'Добавить студию' : 'Добавить место'}
       </h1>
 
-      {error && <p className="rounded-lg bg-red-500/10 p-3 text-sm text-red-400">{error}</p>}
+      {error && (
+        <div className="rounded-lg bg-error-500/10 p-3 text-sm text-error-500">
+          {error}
+        </div>
+      )}
 
-      <div>
-        <label className="mb-1 block text-sm text-white/60">Название *</label>
+      <div className="space-y-2">
+        <label className="text-sm font-medium text-neutral-300">Название *</label>
         <input
           value={title}
           onChange={(e) => setTitle(e.target.value)}
-          className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500"
+          className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
           required
         />
       </div>
 
-      <div>
-        <label className="mb-1 block text-sm text-white/60">Описание</label>
+      <div className="space-y-2">
+        <label className="text-sm font-medium text-neutral-300">Описание</label>
         <textarea
           value={description}
           onChange={(e) => setDescription(e.target.value)}
           rows={4}
-          className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500"
+          className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
         />
       </div>
 
-      <div>
-        <label className="mb-1 block text-sm text-white/60">Адрес</label>
+      <div className="space-y-2">
+        <label className="text-sm font-medium text-neutral-300">Адрес</label>
         <input
           value={address}
           onChange={(e) => setAddress(e.target.value)}
-          className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500"
+          className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
         />
       </div>
 
       <div className="grid grid-cols-5 gap-4">
-        <div className="col-span-2">
-          <label className="mb-1 block text-sm text-white/60">Широта</label>
+        <div className="col-span-2 space-y-2">
+          <label className="text-sm font-medium text-neutral-300">Широта</label>
           <input
             type="number"
             step="any"
             value={lat}
             onChange={(e) => setLat(e.target.value)}
-            className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-3 text-white outline-none focus:border-green-500"
+            className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-3 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
           />
         </div>
-        <div className="col-span-2">
-          <label className="mb-1 block text-sm text-white/60">Долгота</label>
+        <div className="col-span-2 space-y-2">
+          <label className="text-sm font-medium text-neutral-300">Долгота</label>
           <input
             type="number"
             step="any"
             value={lng}
             onChange={(e) => setLng(e.target.value)}
-            className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-3 text-white outline-none focus:border-green-500"
+            className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-3 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
           />
         </div>
         <div className="col-span-1 flex items-end">
           <button
             type="button"
             onClick={() => setShowMapPicker(true)}
-            className="h-[50px] w-auto rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 transition text-center leading-tight"
+            className="h-[50px] w-auto rounded-lg border border-neutral-700 bg-neutral-800 px-3 py-1.5 text-sm text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors text-center leading-tight"
           >
             Указать<br />на карте
           </button>
         </div>
       </div>
 
-      <div>
-        <label className="mb-1 block text-sm text-white/60">Информация о доступе</label>
+      <div className="space-y-2">
+        <label className="text-sm font-medium text-neutral-300">Информация о доступе</label>
         <textarea
           value={accessInfo}
           onChange={(e) => setAccessInfo(e.target.value)}
           rows={2}
-          className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500"
+          className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
           placeholder="Как добраться, парковка..."
         />
       </div>
 
-      <div>
-        <label className="mb-1 block text-sm text-white/60">Обложка</label>
+      <div className="space-y-2">
+        <label className="text-sm font-medium text-neutral-300">Обложка</label>
         <input
           type="file"
           accept="image/*"
@@ -247,7 +251,7 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
               setCoverPreview(URL.createObjectURL(file))
             }
           }}
-          className="w-full text-sm text-white/60 file:mr-4 file:rounded-lg file:border-0 file:bg-green-600 file:px-4 file:py-2 file:text-white file:hover:bg-green-500"
+          className="w-full text-sm text-neutral-400 file:mr-4 file:rounded-lg file:border-0 file:bg-primary-600 file:px-4 file:py-2 file:text-white file:hover:bg-primary-500 file:transition-colors"
         />
         {coverPreview && (
           <img src={coverPreview} alt="preview" className="mt-2 h-32 rounded-lg object-cover" />
@@ -256,40 +260,40 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
 
       {type === 'studio' && (
         <div className="grid grid-cols-2 gap-4">
-          <div>
-            <label className="mb-1 block text-sm text-white/60">Цена за час (₽)</label>
+          <div className="space-y-2">
+            <label className="text-sm font-medium text-neutral-300">Цена за час (₽)</label>
             <input
               type="number"
               value={hourlyRate}
               onChange={(e) => setHourlyRate(e.target.value)}
-              className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500"
+              className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
             />
           </div>
-          <div>
-            <label className="mb-1 block text-sm text-white/60">Мин. часов</label>
+          <div className="space-y-2">
+            <label className="text-sm font-medium text-neutral-300">Мин. часов</label>
             <input
               type="number"
               min="1"
               value={minHours}
               onChange={(e) => setMinHours(e.target.value)}
-              className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white outline-none focus:border-green-500"
+              className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
             />
           </div>
         </div>
       )}
 
-      <div>
-        <label className="mb-2 block text-sm text-white/60">Теги (стили съёмки)</label>
+      <div className="space-y-2">
+        <label className="text-sm font-medium text-neutral-300">Теги (стили съёмки)</label>
         <div className="flex flex-wrap gap-2">
           {tags.map((tag) => (
             <button
               key={tag.id}
               type="button"
               onClick={() => toggleTag(tag.id)}
-              className={`rounded-full px-3 py-1.5 text-sm transition ${
+              className={`rounded-full px-3 py-1.5 text-sm transition-colors ${
                 selectedTags.includes(tag.id)
-                  ? 'bg-green-600 text-white'
-                  : 'bg-white/10 text-white/60 hover:bg-white/20'
+                  ? 'bg-primary-600 text-white'
+                  : 'bg-neutral-800 text-neutral-400 hover:bg-neutral-700 hover:text-neutral-200'
               }`}
             >
               {tag.name}
@@ -298,18 +302,18 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
         </div>
       </div>
 
-      <div>
-        <label className="mb-2 block text-sm text-white/60">Характеристики</label>
+      <div className="space-y-2">
+        <label className="text-sm font-medium text-neutral-300">Характеристики</label>
         <div className="flex flex-wrap gap-2">
           {features.map((feat) => (
             <button
               key={feat.id}
               type="button"
               onClick={() => toggleFeature(feat.id)}
-              className={`rounded-full px-3 py-1.5 text-sm transition ${
+              className={`rounded-full px-3 py-1.5 text-sm transition-colors ${
                 selectedFeatures.includes(feat.id)
-                  ? 'bg-green-600 text-white'
-                  : 'bg-white/10 text-white/60 hover:bg-white/20'
+                  ? 'bg-primary-600 text-white'
+                  : 'bg-neutral-800 text-neutral-400 hover:bg-neutral-700 hover:text-neutral-200'
               }`}
             >
               {feat.name}
@@ -321,11 +325,11 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
       <div className="flex gap-3">
         {!isEdit && (
           <button type="button" onClick={handleSaveDraft} disabled={loading}
-            className="flex-1 rounded-lg border border-white/20 px-4 py-3 font-medium text-white/80 hover:bg-white/5 transition disabled:opacity-50"
+            className="flex-1 rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors disabled:opacity-50"
           >{loading ? 'Сохранение...' : 'Сохранить черновик'}</button>
         )}
         <button type="submit" disabled={loading}
-          className={`rounded-lg bg-green-600 px-4 py-3 font-medium text-white hover:bg-green-500 transition disabled:opacity-50 ${isEdit ? 'w-full' : 'flex-1'}`}
+          className={`rounded-lg bg-primary-600 px-4 py-3 font-medium text-white hover:bg-primary-500 transition-colors disabled:opacity-50 ${isEdit ? 'w-full' : 'flex-1'}`}
         >
           {loading ? 'Сохранение...' : isEdit ? 'Сохранить' : `Опубликовать ${type === 'studio' ? 'студию' : 'место'}`}
         </button>

+ 68 - 49
frontend/src/components/RegisterModal.tsx

@@ -34,72 +34,91 @@ export default function RegisterModal({ onClose }: { onClose: () => void }) {
   }
 
   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
       <div
         onClick={(e) => e.stopPropagation()}
-        className="w-full max-w-sm space-y-4 rounded-2xl bg-green-600/15 p-8 backdrop-blur-md"
+        className="w-full max-w-sm space-y-4 rounded-xl bg-neutral-900 border border-neutral-800 p-8 shadow-xl animate-scale-in"
       >
         {success ? (
           <div className="space-y-2 text-center">
-            <p className="text-lg font-bold text-green-400">✓ Регистрация прошла успешно</p>
-            <p className="text-sm text-white/60">Добро пожаловать в PhotoPlaces!</p>
+            <p className="text-lg font-bold text-success-500">✓ Регистрация прошла успешно</p>
+            <p className="text-sm text-neutral-400">Добро пожаловать в PhotoPlaces Twilight!</p>
           </div>
         ) : (
           <form onSubmit={handleSubmit} className="space-y-4">
-            <h1 className="text-center text-2xl font-bold text-white">Регистрация</h1>
+            <h1 className="text-center text-2xl font-bold text-neutral-100">Регистрация</h1>
 
-            {error && <p className="text-sm text-red-400">{error}</p>}
+            {error && (
+              <div className="rounded-lg bg-error-500/10 p-3 text-sm text-error-500">
+                {error}
+              </div>
+            )}
 
-            <input
-              type="text"
-              placeholder="Имя"
-              value={name}
-              onChange={(e) => setName(e.target.value)}
-              className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/40 outline-none focus:border-green-500"
-            />
+            <div className="space-y-2">
+              <label className="text-sm font-medium text-neutral-300">Имя</label>
+              <input
+                type="text"
+                placeholder="Ваше имя"
+                value={name}
+                onChange={(e) => setName(e.target.value)}
+                className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
+              />
+            </div>
 
-            <input
-              type="email"
-              placeholder="Email"
-              value={email}
-              onChange={(e) => setEmail(e.target.value)}
-              className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/40 outline-none focus:border-green-500"
-              required
-            />
+            <div className="space-y-2">
+              <label className="text-sm font-medium text-neutral-300">Email</label>
+              <input
+                type="email"
+                placeholder="email@example.com"
+                value={email}
+                onChange={(e) => setEmail(e.target.value)}
+                className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
+                required
+              />
+            </div>
 
-            <input
-              type="password"
-              placeholder="Пароль"
-              value={password}
-              onChange={(e) => setPassword(e.target.value)}
-              className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/40 outline-none focus:border-green-500"
-              required
-              minLength={8}
-            />
+            <div className="space-y-2">
+              <label className="text-sm font-medium text-neutral-300">Пароль</label>
+              <input
+                type="password"
+                placeholder="••••••••"
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
+                required
+                minLength={8}
+              />
+            </div>
 
-            <input
-              type="password"
-              placeholder="Повторите пароль"
-              value={confirmPassword}
-              onChange={(e) => setConfirmPassword(e.target.value)}
-              className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/40 outline-none focus:border-green-500"
-              required
-              minLength={8}
-            />
+            <div className="space-y-2">
+              <label className="text-sm font-medium text-neutral-300">Повторите пароль</label>
+              <input
+                type="password"
+                placeholder="••••••••"
+                value={confirmPassword}
+                onChange={(e) => setConfirmPassword(e.target.value)}
+                className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 placeholder-neutral-500 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
+                required
+                minLength={8}
+              />
+            </div>
 
-            <select
-              value={role}
-              onChange={(e) => setRole(e.target.value)}
-              className="w-full rounded-lg border border-white/10 bg-[#15291C] px-4 py-3 text-white outline-none focus:border-green-500"
-            >
-              <option value="customer">Заказчик (ищу места)</option>
-              <option value="landlord">Арендодатель (сдаю студию)</option>
-              <option value="executor">Исполнитель (предлагаю услуги)</option>
-            </select>
+            <div className="space-y-2">
+              <label className="text-sm font-medium text-neutral-300">Роль</label>
+              <select
+                value={role}
+                onChange={(e) => setRole(e.target.value)}
+                className="w-full rounded-lg border border-neutral-700 bg-neutral-800 px-4 py-3 text-neutral-100 outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-colors"
+              >
+                <option value="customer">Заказчик (ищу места)</option>
+                <option value="landlord">Арендодатель (сдаю студию)</option>
+                <option value="executor">Исполнитель (предлагаю услуги)</option>
+              </select>
+            </div>
 
             <button
               type="submit"
-              className="w-full rounded-lg bg-green-600 px-4 py-3 font-medium text-white hover:bg-green-500 transition"
+              className="w-full rounded-lg bg-primary-600 px-4 py-3 font-medium text-white hover:bg-primary-500 transition-colors"
             >
               Зарегистрироваться
             </button>

+ 110 - 13
frontend/src/styles/globals.css

@@ -2,14 +2,91 @@
 @tailwind components;
 @tailwind utilities;
 
+/* PhotoPlaces Twilight Design System */
 :root {
-  --color-primary: #22c55e;
-  --color-primary-dark: #16a34a;
-  --color-bg: #060606;
-  --color-surface: #1e293b;
-  --color-text: #f1f5f9;
-  --color-text-muted: #94a3b8;
-  --color-border: #334155;
+  /* ========== LIGHT THEME ========== */
+  --bg: #FAFAFA;
+  --bg-elevated: #FFFFFF;
+  --bg-hover: #F4F4F5;
+  --border: #E4E4E7;
+  --border-subtle: #F4F4F5;
+  --text-primary: #18181B;
+  --text-secondary: #52525B;
+  --text-muted: #A1A1AA;
+
+  /* Shadows */
+  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
+  --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06);
+  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05);
+}
+
+[data-theme="dark"] {
+  /* ========== DARK THEME ========== */
+  --bg: #09090B;
+  --bg-elevated: #18181B;
+  --bg-hover: #27272A;
+  --border: #3F3F46;
+  --border-subtle: #27272A;
+  --text-primary: #FAFAFA;
+  --text-secondary: #A1A1AA;
+  --text-muted: #71717A;
+
+  /* Shadows */
+  --shadow-sm: 0 1px 2px rgba(0,0,0,0.4);
+  --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4), 0 2px 4px rgba(0,0,0,0.3);
+  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.4), 0 4px 6px rgba(0,0,0,0.3);
+}
+
+:root {
+  /* ========== COLORS ========== */
+  --primary: #6366F1;
+  --primary-hover: #4F46E5;
+  --secondary: #F59E0B;
+  --accent: #06B6D4;
+
+  /* Functional */
+  --success: #22C55E;
+  --warning: #F59E0B;
+  --error: #EF4444;
+  --info: #3B82F6;
+
+  /* ========== TYPOGRAPHY ========== */
+  --font-heading: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
+
+  /* ========== SPACING ========== */
+  --space-0: 0px;
+  --space-1: 4px;
+  --space-2: 8px;
+  --space-3: 12px;
+  --space-4: 16px;
+  --space-5: 20px;
+  --space-6: 24px;
+  --space-8: 32px;
+  --space-10: 40px;
+  --space-12: 48px;
+  --space-16: 64px;
+  --space-20: 80px;
+
+  /* ========== BORDER RADIUS ========== */
+  --radius-sm: 6px;
+  --radius-md: 8px;
+  --radius-lg: 12px;
+  --radius-xl: 16px;
+  --radius-2xl: 24px;
+  --radius-full: 9999px;
+
+  /* ========== TRANSITIONS ========== */
+  --duration-fast: 100ms;
+  --duration-normal: 200ms;
+  --duration-slow: 300ms;
+  --duration-slower: 500ms;
+
+  --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
+  --ease-in: cubic-bezier(0.4, 0, 1, 1);
+  --ease-out: cubic-bezier(0, 0, 0.2, 1);
+  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
 }
 
 * {
@@ -24,9 +101,9 @@ html, body {
 }
 
 body {
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-  background: var(--color-bg);
-  color: var(--color-text);
+  font-family: var(--font-body);
+  background: var(--bg);
+  color: var(--text-primary);
 }
 
 ::-webkit-scrollbar {
@@ -39,15 +116,35 @@ body {
 }
 
 ::-webkit-scrollbar-thumb {
-  background: var(--color-surface);
+  background: var(--bg-elevated);
   border-radius: 4px;
 }
 
 ::-webkit-scrollbar-thumb:hover {
-  background: #475569;
+  background: var(--bg-hover);
 }
 
 * {
   scrollbar-width: thin;
-  scrollbar-color: var(--color-surface) transparent;
+  scrollbar-color: var(--bg-elevated) transparent;
+}
+
+/* ========== CUSTOM UTILITIES ========== */
+@layer utilities {
+  .bg-glass {
+    background: rgba(255, 255, 255, 0.05);
+    backdrop-filter: blur(12px);
+  }
+
+  .bg-glass-strong {
+    background: rgba(255, 255, 255, 0.1);
+    backdrop-filter: blur(16px);
+  }
+
+  .text-gradient {
+    background: linear-gradient(135deg, var(--primary), var(--accent));
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+    background-clip: text;
+  }
 }

+ 127 - 1
frontend/tailwind.config.js

@@ -1,8 +1,134 @@
 /** @type {import('tailwindcss').Config} */
 module.exports = {
   content: ['./src/**/*.{ts,tsx}'],
+  darkMode: ['class', '[data-theme="dark"]'],
   theme: {
-    extend: {},
+    extend: {
+      colors: {
+        primary: {
+          50: '#EEF2FF',
+          100: '#E0E7FF',
+          200: '#C7D2FE',
+          300: '#A5B4FC',
+          400: '#818CF8',
+          500: '#6366F1',
+          600: '#4F46E5',
+          700: '#4338CA',
+          800: '#3730A3',
+          900: '#312E81',
+        },
+        secondary: {
+          50: '#FFFBEB',
+          100: '#FEF3C7',
+          200: '#FDE68A',
+          300: '#FCD34D',
+          400: '#FBBF24',
+          500: '#F59E0B',
+          600: '#D97706',
+          700: '#B45309',
+          800: '#92400E',
+          900: '#78350F',
+        },
+        accent: {
+          50: '#ECFEFF',
+          100: '#CFFAFE',
+          200: '#A5F3FC',
+          300: '#67E8F9',
+          400: '#22D3EE',
+          500: '#06B6D4',
+          600: '#0891B2',
+          700: '#0E7490',
+          800: '#155E75',
+          900: '#164E63',
+        },
+        neutral: {
+          50: '#FAFAFA',
+          100: '#F4F4F5',
+          200: '#E4E4E7',
+          300: '#D4D4D8',
+          400: '#A1A1AA',
+          500: '#71717A',
+          600: '#52525B',
+          700: '#3F3F46',
+          800: '#27272A',
+          900: '#18181B',
+          950: '#09090B',
+        },
+        success: {
+          500: '#22C55E',
+          600: '#16A34A',
+        },
+        warning: {
+          500: '#F59E0B',
+          600: '#D97706',
+        },
+        error: {
+          500: '#EF4444',
+          600: '#DC2626',
+        },
+        info: {
+          500: '#3B82F6',
+          600: '#2563EB',
+        },
+      },
+      fontFamily: {
+        heading: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'],
+        body: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'],
+        mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
+      },
+      fontSize: {
+        'xs': ['12px', { lineHeight: '16px' }],
+        'sm': ['14px', { lineHeight: '20px' }],
+        'base': ['16px', { lineHeight: '24px' }],
+        'lg': ['18px', { lineHeight: '28px' }],
+        'xl': ['20px', { lineHeight: '28px' }],
+        '2xl': ['24px', { lineHeight: '32px' }],
+        '3xl': ['30px', { lineHeight: '36px' }],
+        '4xl': ['36px', { lineHeight: '40px' }],
+        '5xl': ['48px', { lineHeight: '48px' }],
+      },
+      spacing: {
+        '0.5': '2px',
+        '1.5': '6px',
+        '18': '72px',
+        '88': '352px',
+      },
+      borderRadius: {
+        'sm': '6px',
+        'md': '8px',
+        'lg': '12px',
+        'xl': '16px',
+        '2xl': '24px',
+      },
+      boxShadow: {
+        'glow': '0 0 20px rgba(99,102,241,0.3)',
+        'glow-lg': '0 0 40px rgba(99,102,241,0.4)',
+      },
+      animation: {
+        'fade-in': 'fade-in 0.2s ease-out',
+        'slide-up': 'slide-up 0.3s ease-out',
+        'slide-down': 'slide-down 0.3s ease-out',
+        'scale-in': 'scale-in 0.2s ease-out',
+      },
+      keyframes: {
+        'fade-in': {
+          '0%': { opacity: '0' },
+          '100%': { opacity: '1' },
+        },
+        'slide-up': {
+          '0%': { transform: 'translateY(10px)', opacity: '0' },
+          '100%': { transform: 'translateY(0)', opacity: '1' },
+        },
+        'slide-down': {
+          '0%': { transform: 'translateY(-10px)', opacity: '0' },
+          '100%': { transform: 'translateY(0)', opacity: '1' },
+        },
+        'scale-in': {
+          '0%': { transform: 'scale(0.95)', opacity: '0' },
+          '100%': { transform: 'scale(1)', opacity: '1' },
+        },
+      },
+    },
   },
   plugins: [],
 }