diff --git a/src/api/utm.ts b/src/api/utm.ts new file mode 100644 index 00000000..598de597 --- /dev/null +++ b/src/api/utm.ts @@ -0,0 +1,74 @@ +import { makeRequest } from "@frontend/kitui"; +import { parseAxiosError } from "@utils/parse-error"; + +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`; + +export type UtmRecordRaw = { + id: number; + quizID: number; + utm: string; + deleted: boolean; + createdAt: string; // ISO date +}; + +export type UtmRecord = { + id: number; + quiz_id: number; + utm: string; + deleted: boolean; + created_at: string; // ISO date +}; + +function mapUtm(r: UtmRecordRaw): UtmRecord { + return { + id: r.id, + quiz_id: r.quizID, + utm: r.utm, + deleted: r.deleted, + created_at: r.createdAt, + }; +} + +export const utmApi = { + async getAll(quizID: number): Promise<[UtmRecord[] | null, string?]> { + try { + const items = await makeRequest({ + method: "GET", + url: `${API_URL}/utm/${quizID}`, + }); + return [items.map(mapUtm)]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, error]; + } + }, + + async create(body: { quiz_id: number; utm: string }): Promise<[UtmRecord | null, string?]> { + try { + const created = await makeRequest({ + method: "POST", + url: `${API_URL}/utm`, + body, + }); + return [mapUtm(created)]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, error]; + } + }, + + async softDelete(utmID: number): Promise<[true | null, string?]> { + try { + await makeRequest({ + method: "DELETE", + url: `${API_URL}/utm/${utmID}`, + }); + return [true]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, error]; + } + }, +}; + + diff --git a/src/assets/icons/TrashIcon.tsx b/src/assets/icons/TrashIcon.tsx index 1e1ec024..4ff613f0 100644 --- a/src/assets/icons/TrashIcon.tsx +++ b/src/assets/icons/TrashIcon.tsx @@ -1,4 +1,4 @@ -export default function TrashIcon() { +export default function TrashIcon({ color = "#333647" }: { color?: string }) { return ( - + ; @@ -29,6 +30,7 @@ export default function QuizMarkCreate() { const [display, setDisplay] = useState("1"); const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + const [isUtmsOpen, setIsUtmsOpen] = useState(false); const CopyLink = () => { let one = (document.getElementById("inputMarkLinkone") as HTMLInputElement) @@ -216,11 +218,12 @@ export default function QuizMarkCreate() { - + setIsUtmsOpen(true)}> + setIsUtmsOpen(false)} quizBackendId={quiz?.backendId} /> ); diff --git a/src/pages/InstallQuiz/QuizInstallationCard/UtmsModal.tsx b/src/pages/InstallQuiz/QuizInstallationCard/UtmsModal.tsx new file mode 100644 index 00000000..3e1c87fd --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/UtmsModal.tsx @@ -0,0 +1,94 @@ +import { Box, Dialog, IconButton, List, ListItem, ListItemText, Typography, useTheme } from "@mui/material"; +import { FC, useEffect, useState } from "react"; +import { utmApi, type UtmRecord } from "../../../api/utm"; +import TrashIcon from "@/assets/icons/TrashIcon"; + +type UtmsModalProps = { + open: boolean; + onClose: () => void; + quizBackendId?: number; +}; + +export const UtmsModal: FC = ({ open, onClose, quizBackendId }) => { + const theme = useTheme(); + const [loading, setLoading] = useState(false); + const [items, setItems] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + if (open && quizBackendId) { + setLoading(true); + setError(null); + utmApi.getAll(quizBackendId).then(([data, err]) => { + if (cancelled) return; + if (err) { + setError(err); + setItems([]); + } else { + setItems(data ?? []); + } + setLoading(false); + }); + } else if (open && !quizBackendId) { + // нет id — показать пусто + setItems([]); + setLoading(false); + setError(null); + } else { + setItems(null); + setLoading(false); + setError(null); + } + return () => { + cancelled = true; + }; + }, [open, quizBackendId]); + + return ( + + + UTM метки + {/* Создание UTM убрано из модалки. Ввод и сохранение — в карточке. */} + {loading ? ( + Загрузка… + ) : error ? ( + Ошибка загрузки + ) : items && items.length === 0 ? ( + пока пусто + ) : ( + + {(items ?? []).map((u) => ( + { + // клик по записи — копируем utm в буфер + navigator.clipboard.writeText(u.utm); + }} + secondaryAction={ + { + e.stopPropagation(); + const prev = items ?? []; + setItems(prev.filter((it) => it.id !== u.id)); + const [, err] = await utmApi.softDelete(u.id); + if (err) { + // откат при ошибке + setItems(prev); + } + }}> + + + } + > + + + ))} + + )} + + + ); +}; + +