12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env.local
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.DS_Store
|
||||||
|
*.md
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL=string
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,7 +7,11 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
|
#env add gitignore
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
.env.production
|
||||||
|
.example.env
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
|||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
# Copy all source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Set production env (agar .env.production bo‘lsa ishlaydi)
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
RUN npm run build
|
||||||
|
ENTRYPOINT npm run preview
|
||||||
|
|
||||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
simple-travel-admin:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- VITE_API_URL=${VITE_API_URL}
|
||||||
|
- VITE_APP_NAME=${VITE_APP_NAME}
|
||||||
|
container_name: simple-travel-admin
|
||||||
|
ports:
|
||||||
|
- "5263:5263"
|
||||||
|
env_file:
|
||||||
|
- .env.production
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
network:
|
||||||
|
driver: bridge
|
||||||
33
nginx/nginx.conf
Normal file
33
nginx/nginx.conf
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# React Router support
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Don't cache index.html
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ interface NewsData {
|
|||||||
desc: string;
|
desc: string;
|
||||||
title_ru: string;
|
title_ru: string;
|
||||||
desc_ru: string;
|
desc_ru: string;
|
||||||
category: string;
|
category: number | null;
|
||||||
banner: File | undefined | string;
|
banner: File | undefined | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export const useNewsStore = create<NewsStore>((set) => ({
|
|||||||
stepOneData: {
|
stepOneData: {
|
||||||
title: "",
|
title: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
category: "",
|
category: null,
|
||||||
banner: undefined,
|
banner: undefined,
|
||||||
desc_ru: "",
|
desc_ru: "",
|
||||||
title_ru: "",
|
title_ru: "",
|
||||||
@@ -31,7 +31,7 @@ export const useNewsStore = create<NewsStore>((set) => ({
|
|||||||
stepOneData: {
|
stepOneData: {
|
||||||
title: "",
|
title: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
category: "",
|
category: null,
|
||||||
banner: undefined,
|
banner: undefined,
|
||||||
desc_ru: "",
|
desc_ru: "",
|
||||||
title_ru: "",
|
title_ru: "",
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ export const newsForm = z.object({
|
|||||||
desc_ru: z.string().min(2, {
|
desc_ru: z.string().min(2, {
|
||||||
message: "Kamida 2 ta belgidan iborat bo‘lishi kerak.",
|
message: "Kamida 2 ta belgidan iborat bo‘lishi kerak.",
|
||||||
}),
|
}),
|
||||||
category: z.string().min(1, {
|
category: z.number().min(1, {
|
||||||
message: "Majburiy maydon",
|
message: "Majburiy maydon",
|
||||||
}),
|
}),
|
||||||
banner: fileSchema,
|
banner: fileSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// zod schema ni yangilaymiz
|
||||||
export const newsPostForm = z.object({
|
export const newsPostForm = z.object({
|
||||||
desc: z
|
desc: z
|
||||||
.string()
|
.string()
|
||||||
@@ -36,17 +37,22 @@ export const newsPostForm = z.object({
|
|||||||
sections: z
|
sections: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
image: fileSchema,
|
image: z.union([z.instanceof(File), z.string()]).optional(),
|
||||||
text: z.string().min(1, { message: "Matn bo'sh bo'lmasligi kerak." }),
|
text: z.string().optional(),
|
||||||
text_ru: z
|
text_ru: z.string().optional(),
|
||||||
.string()
|
|
||||||
.min(1, { message: "Ruscha matn bo'sh bo'lmasligi kerak." }),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.min(1, { message: "Kamida bitta bo‘lim qo‘shing." }),
|
.optional(),
|
||||||
|
|
||||||
post_tags: z
|
post_tags: z
|
||||||
.array(z.string().min(1, { message: "Teg bo'sh bo'lmasligi kerak." }))
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string().min(1, { message: "Teg bo'sh bo'lmasligi kerak." }),
|
||||||
|
name_ru: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Teg (RU) bo'sh bo'lmasligi kerak." }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
.min(1, { message: "Kamida bitta teg kiriting." }),
|
.min(1, { message: "Kamida bitta teg kiriting." }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -98,12 +98,8 @@ export interface NewsDetail {
|
|||||||
text_ru: string;
|
text_ru: string;
|
||||||
text_uz: string;
|
text_uz: string;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
category: {
|
category: { id: number; name: string; name_ru: string; name_uz: string };
|
||||||
name: string;
|
post_tags: [
|
||||||
name_ru: string;
|
|
||||||
name_uz: string;
|
|
||||||
};
|
|
||||||
tag: [
|
|
||||||
{
|
{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import StepOne from "@/pages/news/ui/StepOne";
|
|||||||
import StepTwo from "@/pages/news/ui/StepTwo";
|
import StepTwo from "@/pages/news/ui/StepTwo";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ const AddNews = () => {
|
|||||||
const isEditMode = useMemo(() => !!id, [id]);
|
const isEditMode = useMemo(() => !!id, [id]);
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data } = useQuery({
|
const { data, refetch } = useQuery({
|
||||||
queryKey: ["news_detail", id],
|
queryKey: ["news_detail", id],
|
||||||
queryFn: () => getDetailNews(Number(id)),
|
queryFn: () => getDetailNews(Number(id)),
|
||||||
select(data) {
|
select(data) {
|
||||||
@@ -22,6 +22,12 @@ const AddNews = () => {
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 w-full mx-auto bg-gray-900 text-white rounded-2xl shadow-lg">
|
<div className="p-8 w-full mx-auto bg-gray-900 text-white rounded-2xl shadow-lg">
|
||||||
<h1 className="text-3xl font-bold mb-6">
|
<h1 className="text-3xl font-bold mb-6">
|
||||||
@@ -41,9 +47,16 @@ const AddNews = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<StepOne isEditMode={isEditMode} setStep={setStep} data={data!} />
|
<StepOne
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
setStep={setStep}
|
||||||
|
data={data!}
|
||||||
|
id={Number(id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === 2 && (
|
||||||
|
<StepTwo key={id} data={data!} isEditMode={isEditMode} id={id!} />
|
||||||
)}
|
)}
|
||||||
{step === 2 && <StepTwo data={data!} isEditMode={isEditMode} id={id!} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { deleteNews, getAllNews } from "@/pages/news/lib/api";
|
import { deleteNews, getAllNews, updateNews } from "@/pages/news/lib/api";
|
||||||
import { Badge } from "@/shared/ui/badge";
|
import { Badge } from "@/shared/ui/badge";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Card } from "@/shared/ui/card";
|
import { Card } from "@/shared/ui/card";
|
||||||
@@ -10,12 +10,15 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Switch } from "@/shared/ui/switch";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Edit,
|
Edit,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Loader2,
|
Loader2,
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
@@ -41,11 +44,33 @@ const News = () => {
|
|||||||
queryFn: () => getAllNews({ page: currentPage, page_size: 10 }),
|
queryFn: () => getAllNews({ page: currentPage, page_size: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate, isPending } = useMutation({
|
const { mutate: deleteMutate, isPending } = useMutation({
|
||||||
mutationFn: (id: number) => deleteNews(id),
|
mutationFn: (id: number) => deleteNews(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setDeleteId(null);
|
setDeleteId(null);
|
||||||
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
||||||
|
toast.success(t("Yangilik o'chirildi"), {
|
||||||
|
richColors: true,
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
richColors: true,
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: togglePublicMutate } = useMutation({
|
||||||
|
mutationFn: ({ id, body }: { id: number; body: FormData }) =>
|
||||||
|
updateNews({ body, id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
||||||
|
toast.success(t("Status o'zgartirildi"), {
|
||||||
|
richColors: true,
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error(t("Xatolik yuz berdi"), {
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
@@ -57,10 +82,21 @@ const News = () => {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (deleteId) {
|
if (deleteId) {
|
||||||
mutate(deleteId);
|
deleteMutate(deleteId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTogglePublic = (id: number, currentStatus: boolean) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
console.log(currentStatus);
|
||||||
|
|
||||||
|
formData.append("is_public", String(currentStatus));
|
||||||
|
togglePublicMutate({
|
||||||
|
id,
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
|
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
|
||||||
@@ -135,10 +171,10 @@ const News = () => {
|
|||||||
allNews?.data.data.results.map((item) => (
|
allNews?.data.data.results.map((item) => (
|
||||||
<Card
|
<Card
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="overflow-hidden bg-neutral-900 hover:bg-neutral-800 transition-all duration-300 border border-neutral-800 hover:border-neutral-700 group"
|
className="overflow-hidden p-0 bg-neutral-900 hover:bg-neutral-800 transition-all duration-300 border border-neutral-800 hover:border-neutral-700 group flex flex-col"
|
||||||
>
|
>
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="relative h-48 overflow-hidden">
|
<div className="relative h-64 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt={item.title}
|
alt={item.title}
|
||||||
@@ -158,7 +194,7 @@ const News = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3 flex-1 flex flex-col">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h2 className="text-xl font-bold line-clamp-2 group-hover:text-blue-400 transition-colors">
|
<h2 className="text-xl font-bold line-clamp-2 group-hover:text-blue-400 transition-colors">
|
||||||
{item.title}
|
{item.title}
|
||||||
@@ -169,21 +205,47 @@ const News = () => {
|
|||||||
{item.text}
|
{item.text}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Date */}
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
||||||
<span>{item.is_public}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Slug */}
|
{/* Slug */}
|
||||||
<div className="pt-2 border-t border-neutral-800">
|
{item.tag && item.tag.length > 0 && (
|
||||||
{item.tag?.map((e) => (
|
<div className="pt-2 border-t border-neutral-800 flex flex-wrap gap-2">
|
||||||
<code className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded">
|
{item.tag.map((e, idx) => (
|
||||||
/{e.name}
|
<code
|
||||||
</code>
|
key={idx}
|
||||||
))}
|
className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
/{e.name}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Spacer to push content to bottom */}
|
||||||
|
<div className="flex-1"></div>
|
||||||
|
|
||||||
|
{/* Public/Private Toggle */}
|
||||||
|
<div className="pt-3 border-t border-neutral-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{item.is_public ? (
|
||||||
|
<Eye size={16} className="text-green-500" />
|
||||||
|
) : (
|
||||||
|
<EyeOff size={16} className="text-gray-500" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-gray-400">
|
||||||
|
{t("Оmmaviy")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={item.is_public}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
handleTogglePublic(item.id, !item.is_public)
|
||||||
|
}
|
||||||
|
className="data-[state=checked]:bg-green-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions - at the very bottom */}
|
||||||
<div className="flex justify-end gap-2 pt-3">
|
<div className="flex justify-end gap-2 pt-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate(`/news/edit/${item.id}`)}
|
onClick={() => navigate(`/news/edit/${item.id}`)}
|
||||||
@@ -240,7 +302,8 @@ const News = () => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
{/* Pagination */}
|
||||||
|
<div className="flex justify-end gap-2 w-[90%] mx-auto mt-8">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ interface Data {
|
|||||||
text_uz: string;
|
text_uz: string;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
category: {
|
category: {
|
||||||
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
name_ru: string;
|
name_ru: string;
|
||||||
name_uz: string;
|
name_uz: string;
|
||||||
};
|
};
|
||||||
tag: [
|
post_tags: [
|
||||||
{
|
{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -63,62 +64,83 @@ const StepOne = ({
|
|||||||
setStep: Dispatch<SetStateAction<number>>;
|
setStep: Dispatch<SetStateAction<number>>;
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
data: Data;
|
data: Data;
|
||||||
|
id: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setStepOneData, stepOneData } = useNewsStore();
|
const { setStepOneData, stepOneData } = useNewsStore();
|
||||||
const hasReset = useRef(false); // 👈 infinite loopni oldini olish uchun
|
const hasReset = useRef(false);
|
||||||
|
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
// News Category fetch - barcha kategoriyalarni bir martada
|
||||||
useInfiniteQuery({
|
const {
|
||||||
queryKey: ["news_category", detail],
|
data: categoryData,
|
||||||
queryFn: ({ pageParam = 1 }) =>
|
fetchNextPage,
|
||||||
getAllNewsCategory({ page: pageParam, page_size: 10 }),
|
hasNextPage,
|
||||||
getNextPageParam: (lastPage) => {
|
isFetchingNextPage,
|
||||||
const currentPage = lastPage.data.data.current_page;
|
isLoading: isCategoriesLoading,
|
||||||
const totalPages = lastPage.data.data.total_pages;
|
} = useInfiniteQuery({
|
||||||
return currentPage < totalPages ? currentPage + 1 : undefined;
|
queryKey: ["news_category"],
|
||||||
},
|
queryFn: ({ pageParam = 1 }) =>
|
||||||
initialPageParam: 1,
|
getAllNewsCategory({ page: pageParam, page_size: 100 }),
|
||||||
});
|
getNextPageParam: (lastPage) => {
|
||||||
|
const currentPage = lastPage.data.data.current_page;
|
||||||
|
const totalPages = lastPage.data.data.total_pages;
|
||||||
|
return currentPage < totalPages ? currentPage + 1 : undefined;
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Avtomatik barcha sahifalarni yuklash
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||||
|
|
||||||
const allCategories =
|
const allCategories =
|
||||||
data?.pages.flatMap((page) => page.data.data.results) ?? [];
|
categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
|
||||||
|
|
||||||
|
// Form setup
|
||||||
const form = useForm<z.infer<typeof newsForm>>({
|
const form = useForm<z.infer<typeof newsForm>>({
|
||||||
resolver: zodResolver(newsForm),
|
resolver: zodResolver(newsForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: stepOneData.title,
|
title: stepOneData.title || "",
|
||||||
category: stepOneData.category,
|
category: stepOneData.category || undefined,
|
||||||
banner: stepOneData.banner,
|
banner: stepOneData.banner,
|
||||||
desc: stepOneData.desc,
|
desc: stepOneData.desc || "",
|
||||||
desc_ru: stepOneData.desc_ru,
|
desc_ru: stepOneData.desc_ru || "",
|
||||||
title_ru: stepOneData.title_ru,
|
title_ru: stepOneData.title_ru || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ reset faqat bir marta, ma'lumot tayyor bo'lganda ishlaydi
|
// Reset form when detail & categories are ready (EDIT MODE)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
detail &&
|
!detail ||
|
||||||
allCategories.length > 0 &&
|
allCategories.length === 0 ||
|
||||||
!hasReset.current // faqat bir marta
|
hasReset.current ||
|
||||||
|
isCategoriesLoading
|
||||||
) {
|
) {
|
||||||
const foundCategory = allCategories.find(
|
return;
|
||||||
(cat) => cat.name === detail.category.name,
|
|
||||||
);
|
|
||||||
|
|
||||||
form.reset({
|
|
||||||
banner: detail.image as any,
|
|
||||||
category: foundCategory ? String(foundCategory.id) : "",
|
|
||||||
title: detail.title_uz,
|
|
||||||
title_ru: detail.title_ru,
|
|
||||||
desc: detail.text_uz,
|
|
||||||
desc_ru: detail.text_ru,
|
|
||||||
});
|
|
||||||
|
|
||||||
hasReset.current = true; // ✅ qayta reset bo‘lmasin
|
|
||||||
}
|
}
|
||||||
}, [detail, allCategories, form]);
|
|
||||||
|
const categoryId = detail.category.id;
|
||||||
|
|
||||||
|
form.reset({
|
||||||
|
banner: detail.image as any,
|
||||||
|
category: categoryId,
|
||||||
|
title: detail.title_uz,
|
||||||
|
title_ru: detail.title_ru,
|
||||||
|
desc: detail.text_uz,
|
||||||
|
desc_ru: detail.text_ru,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kategoriyani alohida set qilish
|
||||||
|
setTimeout(() => {
|
||||||
|
form.setValue("category", categoryId, { shouldValidate: false });
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
hasReset.current = true;
|
||||||
|
}, [detail, allCategories, form, isCategoriesLoading]);
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof newsForm>) {
|
function onSubmit(values: z.infer<typeof newsForm>) {
|
||||||
setStepOneData(values);
|
setStepOneData(values);
|
||||||
@@ -135,7 +157,7 @@ const StepOne = ({
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-6 bg-gray-900"
|
className="space-y-6 bg-gray-900"
|
||||||
>
|
>
|
||||||
{/* title */}
|
{/* Title */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
@@ -154,7 +176,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* title_ru */}
|
{/* Title RU */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title_ru"
|
name="title_ru"
|
||||||
@@ -173,7 +195,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* desc */}
|
{/* Description */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="desc"
|
name="desc"
|
||||||
@@ -192,7 +214,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* desc_ru */}
|
{/* Description RU */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="desc_ru"
|
name="desc_ru"
|
||||||
@@ -211,36 +233,44 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* category */}
|
{/* Category */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="category"
|
name="category"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem>
|
console.log("Category field value:", field.value);
|
||||||
<Label className="text-md">{t("Kategoriya")}</Label>
|
console.log("All categories:", allCategories);
|
||||||
<FormControl>
|
|
||||||
<InfiniteScrollSelect
|
return (
|
||||||
value={field.value}
|
<FormItem>
|
||||||
onValueChange={field.onChange}
|
<Label className="text-md">{t("Kategoriya")}</Label>
|
||||||
placeholder={t("Kategoriya tanlang")}
|
<FormControl>
|
||||||
label={t("Kategoriyalar")}
|
<InfiniteScrollSelect
|
||||||
data={allCategories}
|
value={field.value ? String(field.value) : ""}
|
||||||
hasNextPage={hasNextPage}
|
onValueChange={(value) => {
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
const numValue = Number(value);
|
||||||
fetchNextPage={fetchNextPage}
|
field.onChange(numValue);
|
||||||
renderOption={(cat) => ({
|
}}
|
||||||
key: cat.id,
|
placeholder={t("Kategoriya tanlang")}
|
||||||
value: String(cat.id),
|
label={t("Kategoriyalar")}
|
||||||
label: cat.name,
|
data={allCategories}
|
||||||
})}
|
hasNextPage={hasNextPage}
|
||||||
/>
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
</FormControl>
|
fetchNextPage={fetchNextPage}
|
||||||
<FormMessage />
|
renderOption={(cat) => ({
|
||||||
</FormItem>
|
key: cat.id,
|
||||||
)}
|
value: String(cat.id),
|
||||||
|
label: cat.name,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* banner */}
|
{/* Banner */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="banner"
|
name="banner"
|
||||||
@@ -301,6 +331,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
<div className="w-full flex justify-end">
|
<div className="w-full flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -38,22 +38,18 @@ interface Data {
|
|||||||
name_ru: string;
|
name_ru: string;
|
||||||
name_uz: string;
|
name_uz: string;
|
||||||
};
|
};
|
||||||
tag: [
|
post_tags: Array<{
|
||||||
{
|
id: number;
|
||||||
id: number;
|
name: string;
|
||||||
name: string;
|
name_ru: string;
|
||||||
name_ru: string;
|
name_uz: string;
|
||||||
name_uz: string;
|
}>;
|
||||||
},
|
post_images: Array<{
|
||||||
];
|
image: string;
|
||||||
post_images: [
|
text: string;
|
||||||
{
|
text_ru: string;
|
||||||
image: string;
|
text_uz: string;
|
||||||
text: string;
|
}>;
|
||||||
text_ru: string;
|
|
||||||
text_uz: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepTwo = ({
|
const StepTwo = ({
|
||||||
@@ -77,13 +73,12 @@ const StepTwo = ({
|
|||||||
desc_ru: "",
|
desc_ru: "",
|
||||||
is_public: "yes",
|
is_public: "yes",
|
||||||
sections: [{ image: undefined as any, text: "", text_ru: "" }],
|
sections: [{ image: undefined as any, text: "", text_ru: "" }],
|
||||||
post_tags: [""],
|
post_tags: [{ name: "", name_ru: "" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (detail && !hasReset.current) {
|
if (detail && !hasReset.current) {
|
||||||
// 🧠 xavfsiz map qilish
|
|
||||||
const mappedSections =
|
const mappedSections =
|
||||||
detail.post_images?.map((img) => ({
|
detail.post_images?.map((img) => ({
|
||||||
image: img.image,
|
image: img.image,
|
||||||
@@ -91,13 +86,18 @@ const StepTwo = ({
|
|||||||
text_ru: img.text_ru,
|
text_ru: img.text_ru,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
const mappedTags = detail.tag?.map((t) => t.name_uz) ?? [];
|
const mappedTags =
|
||||||
|
detail.post_tags?.map((t) => ({
|
||||||
|
name: t.name_uz,
|
||||||
|
name_ru: t.name_ru,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
form.reset({
|
form.reset({
|
||||||
desc: detail.text_uz || "",
|
desc: detail.text_uz || "",
|
||||||
desc_ru: detail.text_ru || "",
|
desc_ru: detail.text_ru || "",
|
||||||
is_public: detail.is_public ? "yes" : "no",
|
is_public: detail.is_public ? "yes" : "no",
|
||||||
post_tags: mappedTags.length > 0 ? mappedTags : [""],
|
post_tags:
|
||||||
|
mappedTags.length > 0 ? mappedTags : [{ name: "", name_ru: "" }],
|
||||||
sections:
|
sections:
|
||||||
mappedSections.length > 0
|
mappedSections.length > 0
|
||||||
? mappedSections
|
? mappedSections
|
||||||
@@ -108,18 +108,31 @@ const StepTwo = ({
|
|||||||
}
|
}
|
||||||
}, [detail, form]);
|
}, [detail, form]);
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const {
|
||||||
|
fields: sectionFields,
|
||||||
|
append: appendSection,
|
||||||
|
remove: removeSection,
|
||||||
|
} = useFieldArray({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: "sections",
|
name: "sections",
|
||||||
});
|
});
|
||||||
const { watch, setValue } = form;
|
|
||||||
const postTags = watch("post_tags");
|
const {
|
||||||
const addTag = () => setValue("post_tags", [...postTags, ""]);
|
fields: tagFields,
|
||||||
const removeTag = (i: number) =>
|
append: appendTag,
|
||||||
setValue(
|
remove: removeTag,
|
||||||
"post_tags",
|
} = useFieldArray({
|
||||||
postTags.filter((_, idx) => idx !== i),
|
control: form.control,
|
||||||
);
|
name: "post_tags",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleImageChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) form.setValue(`sections.${index}.image`, file);
|
||||||
|
};
|
||||||
|
|
||||||
const { mutate: added } = useMutation({
|
const { mutate: added } = useMutation({
|
||||||
mutationFn: (body: FormData) => addNews(body),
|
mutationFn: (body: FormData) => addNews(body),
|
||||||
@@ -139,7 +152,7 @@ const StepTwo = ({
|
|||||||
|
|
||||||
const { mutate: update } = useMutation({
|
const { mutate: update } = useMutation({
|
||||||
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
||||||
updateNews({ id: id, body }),
|
updateNews({ id, body }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["news_detail"] });
|
queryClient.refetchQueries({ queryKey: ["news_detail"] });
|
||||||
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
||||||
@@ -154,14 +167,6 @@ const StepTwo = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleImageChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
index: number,
|
|
||||||
) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) form.setValue(`sections.${index}.image`, file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (values: NewsPostFormType) => {
|
const onSubmit = (values: NewsPostFormType) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
@@ -176,28 +181,27 @@ const StepTwo = ({
|
|||||||
formData.append("image", stepOneData.banner);
|
formData.append("image", stepOneData.banner);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 sections
|
// Sections
|
||||||
values.sections.forEach((section, i) => {
|
values.sections?.forEach((section, i) => {
|
||||||
if (section.image instanceof File) {
|
if (section.image instanceof File)
|
||||||
formData.append(`post_images[${i}]`, section.image);
|
formData.append(`post_images[${i}]`, section.image);
|
||||||
}
|
if (section.text) formData.append(`post_text[${i}]`, section.text);
|
||||||
formData.append(`post_text[${i}]`, section.text);
|
if (section.text_ru)
|
||||||
formData.append(`post_text_ru[${i}]`, section.text_ru);
|
formData.append(`post_text_ru[${i}]`, section.text_ru);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Post Tags
|
||||||
values.post_tags.forEach((tag, i) => {
|
values.post_tags.forEach((tag, i) => {
|
||||||
formData.append(`post_tags[${i}]`, tag);
|
formData.append(`post_tags[${i}]name`, tag.name);
|
||||||
|
formData.append(`post_tags[${i}]name_ru`, tag.name_ru);
|
||||||
});
|
});
|
||||||
if (id) {
|
|
||||||
update({
|
if (id) update({ body: formData, id: Number(id) });
|
||||||
body: formData,
|
else added(formData);
|
||||||
id: Number(id),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
added(formData);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(form.formState.errors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -239,45 +243,58 @@ const StepTwo = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Post Tags */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label>{t("Teglar")}</Label>
|
<Label>{t("Teglar")}</Label>
|
||||||
{postTags.map((__, i) => (
|
{tagFields.map((field, i) => (
|
||||||
<FormField
|
<div key={field.id} className="flex gap-2 items-start">
|
||||||
key={i}
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`post_tags.${i}`}
|
name={`post_tags.${i}.name`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="relative">
|
|
||||||
{postTags.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeTag(i)}
|
|
||||||
className="absolute top-1 right-1 text-red-400 hover:text-red-500"
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder={t("Masalan: sport")} />
|
<Input {...field} placeholder={t("Teg (UZ)")} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</div>
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`post_tags.${i}.name_ru`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder={t("Teg (RU)")} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{tagFields.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeTag(i)}
|
||||||
|
className="text-red-400 hover:text-red-500 mt-1"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addTag}
|
onClick={() => appendTag({ name: "", name_ru: "" })}
|
||||||
className="bg-gray-600 hover:bg-gray-700"
|
className="bg-gray-600 hover:bg-gray-700 mt-2"
|
||||||
>
|
>
|
||||||
<PlusCircle className="size-5 mr-2" />
|
<PlusCircle className="size-5 mr-2" />
|
||||||
{t("Teg qo'shish")}
|
{t("Teg qo'shish")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fields.map((field, index) => (
|
{/* Sections */}
|
||||||
|
{sectionFields.map((field, index) => (
|
||||||
<div
|
<div
|
||||||
key={field.id}
|
key={field.id}
|
||||||
className="border border-gray-700 rounded-lg p-4 space-y-4"
|
className="border border-gray-700 rounded-lg p-4 space-y-4"
|
||||||
@@ -288,7 +305,7 @@ const StepTwo = ({
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove(index)}
|
onClick={() => removeSection(index)}
|
||||||
className="text-red-400 hover:text-red-500"
|
className="text-red-400 hover:text-red-500"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
@@ -341,7 +358,7 @@ const StepTwo = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text (UZ) */}
|
{/* Text UZ */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`sections.${index}.text`}
|
name={`sections.${index}.text`}
|
||||||
@@ -355,7 +372,7 @@ const StepTwo = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Text (RU) */}
|
{/* Text RU */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`sections.${index}.text_ru`}
|
name={`sections.${index}.text_ru`}
|
||||||
@@ -374,7 +391,7 @@ const StepTwo = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
append({ image: undefined as any, text: "", text_ru: "" })
|
appendSection({ image: undefined as any, text: "", text_ru: "" })
|
||||||
}
|
}
|
||||||
className="bg-gray-700 hover:bg-gray-600"
|
className="bg-gray-700 hover:bg-gray-600"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -530,5 +530,8 @@
|
|||||||
"Yuborish": "Отправить",
|
"Yuborish": "Отправить",
|
||||||
"Pulni o'tkazish": "Перевод средств",
|
"Pulni o'tkazish": "Перевод средств",
|
||||||
"To‘lov muvaffaqiyatli amalga oshirildi!": "Оплата успешно выполнена!",
|
"To‘lov muvaffaqiyatli amalga oshirildi!": "Оплата успешно выполнена!",
|
||||||
"Sizga tizimga kirishga ruxsat berilmagan": "Вам не разрешен доступ к системе."
|
"Sizga tizimga kirishga ruxsat berilmagan": "Вам не разрешен доступ к системе.",
|
||||||
|
"Оmmaviy": "Публичный",
|
||||||
|
"Shaxsiy": "Личный",
|
||||||
|
"Status o'zgartirildi": "Статус изменён"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -531,5 +531,8 @@
|
|||||||
"Yuborish": "Yuborish",
|
"Yuborish": "Yuborish",
|
||||||
"Pulni o'tkazish": "Pulni o'tkazish",
|
"Pulni o'tkazish": "Pulni o'tkazish",
|
||||||
"To‘lov muvaffaqiyatli amalga oshirildi!": "To‘lov muvaffaqiyatli amalga oshirildi!",
|
"To‘lov muvaffaqiyatli amalga oshirildi!": "To‘lov muvaffaqiyatli amalga oshirildi!",
|
||||||
"Sizga tizimga kirishga ruxsat berilmagan": "Sizga tizimga kirishga ruxsat berilmagan"
|
"Sizga tizimga kirishga ruxsat berilmagan": "Sizga tizimga kirishga ruxsat berilmagan",
|
||||||
|
"Оmmaviy": "Ommaviy",
|
||||||
|
"Shaxsiy": "Shaxsiy",
|
||||||
|
"Status o'zgartirildi": "Status o'zgartirildi"
|
||||||
}
|
}
|
||||||
|
|||||||
11
stack.yaml
Normal file
11
stack.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
simple-travel-front-admin:
|
||||||
|
image: muhammadvadud/simple-travel-front-admin:latest
|
||||||
|
ports:
|
||||||
|
- "5263:3000"
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
@@ -19,6 +19,12 @@ export default defineConfig({
|
|||||||
host: true,
|
host: true,
|
||||||
port: 5173,
|
port: 5173,
|
||||||
},
|
},
|
||||||
|
preview: {
|
||||||
|
host: true, // Production (vite preview) uchun
|
||||||
|
port: 5263,
|
||||||
|
allowedHosts: ["admin.simpletravel.uz"], // ✅ bu yer muhim
|
||||||
|
},
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist", // Vercel build chiqishini shu papkadan oladi
|
outDir: "dist", // Vercel build chiqishini shu papkadan oladi
|
||||||
sourcemap: false, // Agar kerak bo‘lmasa o‘chirib qo‘ying
|
sourcemap: false, // Agar kerak bo‘lmasa o‘chirib qo‘ying
|
||||||
|
|||||||
Reference in New Issue
Block a user