ютм с дизайном и беком, но без логики
This commit is contained in:
parent
739eb32d23
commit
9c61a9e2b8
74
src/api/utm.ts
Normal file
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 (
|
||||
<svg
|
||||
width="30"
|
||||
@ -7,7 +7,7 @@ export default function TrashIcon() {
|
||||
fill="none"
|
||||
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
|
||||
d="M23.25 9.0625H6.75"
|
||||
stroke="white"
|
||||
|
||||
@ -20,6 +20,7 @@ import CopyIcon from "@/assets/icons/CopyIcon";
|
||||
import LinkIcon from "@/assets/icons/LinkIcon";
|
||||
import { ReactComponent as PlusIco } from "@/assets/icons/PlusIco.svg";
|
||||
import { InfoPopover } from "@/ui_kit/InfoPopover";
|
||||
import { UtmsModal } from "./UtmsModal";
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
|
||||
|
||||
|
||||
@ -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() {
|
||||
</Box>
|
||||
|
||||
<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>
|
||||
</IconButton>
|
||||
<Button variant="contained" sx={{ width: "133px", height: "44px", margin: "0px 0 0 auto" }}>Сохранить</Button>
|
||||
</Box>
|
||||
<UtmsModal open={isUtmsOpen} onClose={() => setIsUtmsOpen(false)} quizBackendId={quiz?.backendId} />
|
||||
</Paper>
|
||||
);
|
||||
|
||||
|
||||
94
src/pages/InstallQuiz/QuizInstallationCard/UtmsModal.tsx
Normal file
94
src/pages/InstallQuiz/QuizInstallationCard/UtmsModal.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user