ютм с дизайном и беком, но без логики

This commit is contained in:
Nastya 2025-10-06 11:50:54 +03:00
parent 739eb32d23
commit 9c61a9e2b8
4 changed files with 174 additions and 3 deletions

74
src/api/utm.ts Normal file

@ -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<unknown, UtmRecordRaw[]>({
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<typeof body, UtmRecordRaw>({
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<unknown, unknown>({
method: "DELETE",
url: `${API_URL}/utm/${utmID}`,
});
return [true];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, error];
}
},
};

@ -1,4 +1,4 @@
export default function TrashIcon() { export default function TrashIcon({ color = "#333647" }: { color?: string }) {
return ( return (
<svg <svg
width="30" width="30"
@ -7,7 +7,7 @@ export default function TrashIcon() {
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<rect y="0.8125" width="30" height="30" rx="6" fill="#333647" /> <rect y="0.8125" width="30" height="30" rx="6" fill={color} />
<path <path
d="M23.25 9.0625H6.75" d="M23.25 9.0625H6.75"
stroke="white" stroke="white"

@ -20,6 +20,7 @@ import CopyIcon from "@/assets/icons/CopyIcon";
import LinkIcon from "@/assets/icons/LinkIcon"; import LinkIcon from "@/assets/icons/LinkIcon";
import { ReactComponent as PlusIco } from "@/assets/icons/PlusIco.svg"; import { ReactComponent as PlusIco } from "@/assets/icons/PlusIco.svg";
import { InfoPopover } from "@/ui_kit/InfoPopover"; import { InfoPopover } from "@/ui_kit/InfoPopover";
import { UtmsModal } from "./UtmsModal";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -29,6 +30,7 @@ export default function QuizMarkCreate() {
const [display, setDisplay] = useState("1"); const [display, setDisplay] = useState("1");
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [isUtmsOpen, setIsUtmsOpen] = useState(false);
const CopyLink = () => { const CopyLink = () => {
let one = (document.getElementById("inputMarkLinkone") as HTMLInputElement) let one = (document.getElementById("inputMarkLinkone") as HTMLInputElement)
@ -216,11 +218,12 @@ export default function QuizMarkCreate() {
</Box> </Box>
<Box sx={{ display: "inline-flex", margin: "11px 0 0 0", alignItems: "flex-end" }}> <Box sx={{ display: "inline-flex", margin: "11px 0 0 0", alignItems: "flex-end" }}>
<IconButton sx={{ borderRadius: "6px", padding: "0px 0px 0px 0px" }}> <IconButton sx={{ borderRadius: "6px", padding: "0px 0px 0px 0px" }} onClick={() => setIsUtmsOpen(true)}>
<PlusIco></PlusIco> <PlusIco></PlusIco>
</IconButton> </IconButton>
<Button variant="contained" sx={{ width: "133px", height: "44px", margin: "0px 0 0 auto" }}>Сохранить</Button> <Button variant="contained" sx={{ width: "133px", height: "44px", margin: "0px 0 0 auto" }}>Сохранить</Button>
</Box> </Box>
<UtmsModal open={isUtmsOpen} onClose={() => setIsUtmsOpen(false)} quizBackendId={quiz?.backendId} />
</Paper> </Paper>
); );

@ -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<UtmsModalProps> = ({ open, onClose, quizBackendId }) => {
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [items, setItems] = useState<UtmRecord[] | null>(null);
const [error, setError] = useState<string | null>(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 (
<Dialog open={open} onClose={onClose} PaperProps={{ sx: { borderRadius: "12px" } }}>
<Box sx={{ p: 3, minWidth: 360 }}>
<Typography sx={{ fontWeight: 600, mb: 2 }}>UTM метки</Typography>
{/* Создание UTM убрано из модалки. Ввод и сохранение — в карточке. */}
{loading ? (
<Typography sx={{ color: theme.palette.grey2.main }}>Загрузка</Typography>
) : error ? (
<Typography sx={{ color: theme.palette.error.main }}>Ошибка загрузки</Typography>
) : items && items.length === 0 ? (
<Typography sx={{ color: theme.palette.grey2.main }}>пока пусто</Typography>
) : (
<List>
{(items ?? []).map((u) => (
<ListItem
key={u.id}
button
onClick={() => {
// клик по записи — копируем utm в буфер
navigator.clipboard.writeText(u.utm);
}}
secondaryAction={
<IconButton edge="end" aria-label="delete" onClick={async (e) => {
e.stopPropagation();
const prev = items ?? [];
setItems(prev.filter((it) => it.id !== u.id));
const [, err] = await utmApi.softDelete(u.id);
if (err) {
// откат при ошибке
setItems(prev);
}
}}>
<TrashIcon color="#FB5607" />
</IconButton>
}
>
<ListItemText primary={u.utm} secondary={`ID: ${u.id}`} />
</ListItem>
))}
</List>
)}
</Box>
</Dialog>
);
};