Преглед на файлове

add draft place support

Backend: accept optional status field on POST /places, default to pending_moderation
Frontend: add 'Save as draft' button on PlaceForm, add 'draft' filter tab on My Places
neyrogovnarik преди 1 месец
родител
ревизия
bbe9ad1a0b
променени са 4 файла, в които са добавени 28 реда и са изтрити 10 реда
  1. 2 0
      backend/internal/handlers/places.go
  2. 7 1
      backend/internal/services/places.go
  3. 1 0
      frontend/src/app/places/my/page.tsx
  4. 18 9
      frontend/src/components/PlaceForm.tsx

+ 2 - 0
backend/internal/handlers/places.go

@@ -178,6 +178,7 @@ type createPlaceRequest struct {
 	HourlyRate  *int     `json:"hourly_rate" validate:"omitempty,min=0"`
 	Currency    string   `json:"currency" validate:"omitempty,currency,len=3"`
 	MinHours    int      `json:"min_hours" validate:"min=0,max=100"`
+	Status      string   `json:"status" validate:"omitempty,place_status"`
 }
 
 type updatePlaceRequest struct {
@@ -249,6 +250,7 @@ func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
 		HourlyRate:  req.HourlyRate,
 		Currency:    req.Currency,
 		MinHours:    req.MinHours,
+		Status:      req.Status,
 	})
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, "failed to create place", err)

+ 7 - 1
backend/internal/services/places.go

@@ -81,9 +81,15 @@ type CreatePlaceInput struct {
 	MinHours   int
 	Tags       []models.Tag
 	Features   []models.Feature
+	Status     string
 }
 
 func (s *PlaceService) Create(ctx context.Context, input CreatePlaceInput) (*models.Place, error) {
+	status := input.Status
+	if status == "" {
+		status = "pending_moderation"
+	}
+
 	place := &models.Place{
 		Type:       input.Type,
 		OwnerID:    input.OwnerID,
@@ -94,7 +100,7 @@ func (s *PlaceService) Create(ctx context.Context, input CreatePlaceInput) (*mod
 		Lng:        input.Lng,
 		CoverImage: input.CoverImage,
 		AccessInfo: input.AccessInfo,
-		Status:     "pending_moderation",
+		Status:     status,
 		HourlyRate: input.HourlyRate,
 		Currency:   input.Currency,
 		MinHours:   input.MinHours,

+ 1 - 0
frontend/src/app/places/my/page.tsx

@@ -250,6 +250,7 @@ function PlaceEditModal({ placeId, onClose, onSaved }: { placeId: string; onClos
 
 const FILTERS: { key: string; label: string }[] = [
   { key: 'all', label: 'Все' },
+  { key: 'draft', label: 'Черновики' },
   { key: 'published', label: 'Опубликовано' },
   { key: 'pending_moderation', label: 'На модерации' },
   { key: 'revision', label: 'На доработке' },

+ 18 - 9
frontend/src/components/PlaceForm.tsx

@@ -68,8 +68,7 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
     setShowMapPicker(false)
   }
 
-  const handleSubmit = async (e: React.FormEvent) => {
-    e.preventDefault()
+  const submitPlace = async (status?: string) => {
     setLoading(true)
     setError('')
 
@@ -102,6 +101,8 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
         body.currency = 'RUB'
       }
 
+      if (status) body.status = status
+
       if (isEdit) {
         await api.patch(`/places/${place.id}`, body)
       } else {
@@ -115,6 +116,9 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
     }
   }
 
+  const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); submitPlace() }
+  const handleSaveDraft = () => submitPlace('draft')
+
   const toggleTag = (id: string) => {
     setSelectedTags((prev) =>
       prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]
@@ -292,13 +296,18 @@ export default function PlaceForm({ type, onSuccess, place }: PlaceFormProps) {
         </div>
       </div>
 
-      <button
-        type="submit"
-        disabled={loading}
-        className="w-full rounded-lg bg-green-600 px-4 py-3 font-medium text-white hover:bg-green-500 transition disabled:opacity-50"
-      >
-        {loading ? 'Сохранение...' : isEdit ? 'Сохранить' : `Создать ${type === 'studio' ? 'студию' : 'место'}`}
-      </button>
+      <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"
+          >{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'}`}
+        >
+          {loading ? 'Сохранение...' : isEdit ? 'Сохранить' : `Опубликовать ${type === 'studio' ? 'студию' : 'место'}`}
+        </button>
+      </div>
 
       {showMapPicker && <MapPicker onSelect={handleMapPick} onClose={() => setShowMapPicker(false)} />}
     </form>