✨ Add button to PassagesDisplay to disable passages fetching
This commit is contained in:
19
frontend/src/extra/iconHamburgerMenu.tsx
Normal file
19
frontend/src/extra/iconHamburgerMenu.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createIcon } from "@hope-ui/solid";
|
||||||
|
|
||||||
|
|
||||||
|
// From https://github.com/hope-ui/hope-ui/blob/main/apps/docs/src/icons/IconHamburgerMenu.tsx
|
||||||
|
|
||||||
|
export const IconHamburgerMenu = createIcon({
|
||||||
|
viewBox: "0 0 15 15",
|
||||||
|
path: () => (
|
||||||
|
<path
|
||||||
|
d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386
|
||||||
|
13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5
|
||||||
|
8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761
|
||||||
|
13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
@@ -39,6 +39,21 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header .menu {
|
||||||
|
height: calc(80/100*100%);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: calc(30/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .menu button {
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
|
||||||
|
background-color: var(--idfm-black);
|
||||||
|
border: solid var(--idfm-white) 3px;
|
||||||
|
border-radius: calc(9/86*100%);
|
||||||
|
}
|
||||||
|
|
||||||
.header .clock {
|
.header .clock {
|
||||||
width: calc(175/1920*100%);
|
width: calc(175/1920*100%);
|
||||||
height: calc(80/100*100%);
|
height: calc(80/100*100%);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { createEffect, createResource, createSignal, For, JSX, ParentComponent, Show, useContext, VoidComponent } from "solid-js";
|
import { createContext, createEffect, createResource, createSignal, For, JSX, ParentComponent, Show, useContext, VoidComponent, VoidProps } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { createDateNow } from "@solid-primitives/date";
|
import { createDateNow } from "@solid-primitives/date";
|
||||||
|
import { IconButton, Menu, MenuTrigger, MenuContent, MenuItem } from "@hope-ui/solid";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||||
@@ -9,45 +10,224 @@ import { SearchContext, SearchStore } from "./search";
|
|||||||
import { Passage, Passages } from "./types";
|
import { Passage, Passages } from "./types";
|
||||||
import { getTransportModeSrc } from "./utils";
|
import { getTransportModeSrc } from "./utils";
|
||||||
import { PassagesPanel } from "./passagesPanel";
|
import { PassagesPanel } from "./passagesPanel";
|
||||||
|
import { IconHamburgerMenu } from './extra/iconHamburgerMenu';
|
||||||
|
|
||||||
import styles from "./passagesDisplay.module.css";
|
import styles from "./passagesDisplay.module.css";
|
||||||
|
|
||||||
|
|
||||||
export const PassagesDisplay: ParentComponent = () => {
|
type PositionnedPanel = {
|
||||||
|
position: number;
|
||||||
|
// TODO: Should be PassagesPanelComponent ?
|
||||||
|
panel: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
const maxPassagePerPanel = 5;
|
|
||||||
const syncPeriodMsec = 20 * 1000;
|
interface PassagesDisplayStore {
|
||||||
const panelSwitchPeriodMsec = 4 * 1000;
|
isPassagesRefreshEnabled: () => boolean;
|
||||||
|
enablePassagesRefresh: () => void;
|
||||||
|
disablePassagesRefresh: () => void;
|
||||||
|
togglePassagesRefresh: () => void;
|
||||||
|
|
||||||
|
getPanels: () => Array<PositionnedPanel>;
|
||||||
|
setPanels: (panels: Array<PositionnedPanel>) => void;
|
||||||
|
|
||||||
|
getDisplayedPanelId: () => number;
|
||||||
|
setDisplayedPanelId: (panelId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PassagesDisplayContext = createContext<PassagesDisplayStore>();
|
||||||
|
|
||||||
|
function PassagesDisplayProvider(props: { children: JSX.Element }) {
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
refreshEnabled: boolean;
|
||||||
|
panels: Array<PositionnedPanel>;
|
||||||
|
displayedPanelId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [store, setStore] = createStore<Store>({ refreshEnabled: true, panels: [], displayedPanelId: 0 });
|
||||||
|
|
||||||
|
const isPassagesRefreshEnabled = (): boolean => {
|
||||||
|
return store.refreshEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enablePassagesRefresh = (): void => {
|
||||||
|
setStore('refreshEnabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const disablePassagesRefresh = (): void => {
|
||||||
|
setStore('refreshEnabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePassagesRefresh = (): void => {
|
||||||
|
setStore('refreshEnabled', !store.refreshEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPanels = (): Array<PositionnedPanel> => {
|
||||||
|
return store.panels;
|
||||||
|
}
|
||||||
|
const setPanels = (panels: Array<PositionnedPanel>): void => {
|
||||||
|
setStore('panels', panels);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDisplayedPanelId = (): number => {
|
||||||
|
return store.displayedPanelId;
|
||||||
|
}
|
||||||
|
const setDisplayedPanelId = (panelId: number): void => {
|
||||||
|
setStore('displayedPanelId', panelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PassagesDisplayContext.Provider value={{
|
||||||
|
isPassagesRefreshEnabled, enablePassagesRefresh,
|
||||||
|
disablePassagesRefresh, togglePassagesRefresh,
|
||||||
|
getPanels, setPanels,
|
||||||
|
getDisplayedPanelId, setDisplayedPanelId
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</PassagesDisplayContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Sort transport modes by weight
|
||||||
|
const Header: VoidComponent<{ title: string }> = (props) => {
|
||||||
|
|
||||||
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||||
// TODO: Use props instead
|
const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext);
|
||||||
const searchStore: SearchStore | undefined = useContext(SearchContext);
|
|
||||||
|
|
||||||
if (businessDataStore === undefined || searchStore === undefined)
|
if (businessDataStore === undefined || passagesDisplayStore === undefined)
|
||||||
return <div />;
|
return <div />;
|
||||||
|
|
||||||
const { passages, getLine, getLinePassages, refreshPassages, clearPassages } = businessDataStore;
|
const { getLine, passages } = businessDataStore;
|
||||||
const { getDisplayedStops } = searchStore;
|
const { isPassagesRefreshEnabled, togglePassagesRefresh } = passagesDisplayStore;
|
||||||
|
|
||||||
const [displayedPanelId, setDisplayedPanelId] = createSignal<number>(0);
|
|
||||||
|
|
||||||
type PositionnedPanel = {
|
|
||||||
position: number;
|
|
||||||
// TODO: Should be PassagesPanelComponent ?
|
|
||||||
panel: JSX.Element;
|
|
||||||
};
|
|
||||||
const [panels, setPanels] = createStore<PositionnedPanel[]>([]);
|
|
||||||
|
|
||||||
const [dateNow] = createDateNow(1000);
|
const [dateNow] = createDateNow(1000);
|
||||||
|
|
||||||
|
const computeTransportModes = async (lineIds: string[]): Promise<string[]> => {
|
||||||
|
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId)));
|
||||||
|
const urls: Set<string> = new Set();
|
||||||
|
for (const line of lines) {
|
||||||
|
const src = getTransportModeSrc(line.transportMode, false);
|
||||||
|
if (src !== undefined) {
|
||||||
|
urls.add(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(urls);
|
||||||
|
}
|
||||||
|
const [linesIds, setLinesIds] = createSignal<string[]>([]);
|
||||||
|
const [transportModeUrls] = createResource<string[], string[]>(linesIds, computeTransportModes);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
setLinesIds(Object.keys(passages()));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.header}>
|
||||||
|
<Show when={transportModeUrls() !== undefined} >
|
||||||
|
<For each={transportModeUrls()}>
|
||||||
|
{(url) =>
|
||||||
|
<div class={styles.transportMode}>
|
||||||
|
<img src={url} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
<div class={styles.title}>
|
||||||
|
<svg viewBox="0 0 1260 50">
|
||||||
|
<text x="0" y="50%" dominant-baseline="middle" font-size="50" style="fill: #ffffff">
|
||||||
|
{props.title}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class={styles.menu}>
|
||||||
|
<Menu>
|
||||||
|
<MenuTrigger
|
||||||
|
as={IconButton}
|
||||||
|
icon=<IconHamburgerMenu />
|
||||||
|
/>
|
||||||
|
<MenuContent>
|
||||||
|
<MenuItem onSelect={() => togglePassagesRefresh()}>{isPassagesRefreshEnabled() ? "Disable" : "Enable"}</MenuItem>
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<div class={styles.clock}>
|
||||||
|
<svg viewBox="0 0 115 43">
|
||||||
|
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-size="43" style="fill: #ffffff">
|
||||||
|
{format(dateNow(), "HH:mm")}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Footer: VoidComponent<{}> = () => {
|
||||||
|
|
||||||
|
const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext);
|
||||||
|
|
||||||
|
if (passagesDisplayStore === undefined)
|
||||||
|
return <div />;
|
||||||
|
|
||||||
|
const { getDisplayedPanelId, getPanels } = passagesDisplayStore;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.footer}>
|
||||||
|
<For each={getPanels()}>
|
||||||
|
{(panel) => {
|
||||||
|
const position = panel.position;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<svg viewBox="0 0 29 29">
|
||||||
|
<circle cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3"
|
||||||
|
style={{ fill: `var(--idfm-${position == getDisplayedPanelId() ? "white" : "black"})` }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Body: ParentComponent<{ maxPassagesPerPanel: number, syncPeriodMsec: number, panelSwitchPeriodMsec: number }> = (props) => {
|
||||||
|
|
||||||
|
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||||
|
const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext);
|
||||||
|
// TODO: Use props instead
|
||||||
|
const searchStore: SearchStore | undefined = useContext(SearchContext);
|
||||||
|
|
||||||
|
if (businessDataStore === undefined || passagesDisplayStore === undefined || searchStore === undefined) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getLinePassages, passages, clearPassages, refreshPassages } = businessDataStore;
|
||||||
|
const { isPassagesRefreshEnabled, getDisplayedPanelId, setDisplayedPanelId, getPanels, setPanels } = passagesDisplayStore;
|
||||||
|
const { getDisplayedStops } = searchStore;
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
let nextPanelId = displayedPanelId() + 1;
|
let nextPanelId = getDisplayedPanelId() + 1;
|
||||||
if (nextPanelId >= panels.length) {
|
if (nextPanelId >= getPanels().length) {
|
||||||
nextPanelId = 0;
|
nextPanelId = 0;
|
||||||
}
|
}
|
||||||
setDisplayedPanelId(nextPanelId);
|
setDisplayedPanelId(nextPanelId);
|
||||||
}, panelSwitchPeriodMsec);
|
}, props.panelSwitchPeriodMsec);
|
||||||
|
|
||||||
|
setInterval(
|
||||||
|
async () => {
|
||||||
|
if (isPassagesRefreshEnabled()) {
|
||||||
|
const stops = getDisplayedStops();
|
||||||
|
if (stops.length > 0) {
|
||||||
|
refreshPassages(stops[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("Passages refresh disabled... skip it.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props.syncPeriodMsec
|
||||||
|
);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log("######### onStopIdUpdate #########");
|
console.log("######### onStopIdUpdate #########");
|
||||||
@@ -64,137 +244,67 @@ export const PassagesDisplay: ParentComponent = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(
|
return (
|
||||||
async () => {
|
<div class={styles.panelsContainer}>
|
||||||
const stops = getDisplayedStops();
|
{() => {
|
||||||
if (stops.length > 0) {
|
setPanels([]);
|
||||||
refreshPassages(stops[0].id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syncPeriodMsec
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Sort transport modes by weight
|
let newPanels = [];
|
||||||
const Header: VoidComponent<{ passages: Passages, title: string }> = (props) => {
|
let positioneds: PositionnedPanel[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
const computeTransportModes = async (lineIds: string[]): Promise<string[]> => {
|
let chunk: Record<string, Record<string, Passage[]>> = {};
|
||||||
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId)));
|
let chunkSize = 0;
|
||||||
const urls: Set<string> = new Set();
|
|
||||||
for (const line of lines) {
|
for (const lineId of Object.keys(passages())) {
|
||||||
const src = getTransportModeSrc(line.transportMode, false);
|
const byLinePassages = getLinePassages(lineId);
|
||||||
if (src !== undefined) {
|
const byLinePassagesKeys = Object.keys(byLinePassages);
|
||||||
urls.add(src);
|
|
||||||
|
if (byLinePassagesKeys.length <= props.maxPassagesPerPanel - chunkSize) {
|
||||||
|
chunk[lineId] = byLinePassages;
|
||||||
|
chunkSize += byLinePassagesKeys.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const panelid = index++;
|
||||||
|
const panel = <PassagesPanel show={panelid == getDisplayedPanelId()} passages={chunk} />;
|
||||||
|
newPanels.push(panel);
|
||||||
|
positioneds.push({ position: panelid, panel: panel });
|
||||||
|
|
||||||
|
chunk = {};
|
||||||
|
chunk[lineId] = byLinePassages;
|
||||||
|
chunkSize = byLinePassagesKeys.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunkSize) {
|
||||||
|
const panelId = index++;
|
||||||
|
const panel = <PassagesPanel show={panelId == getDisplayedPanelId()} passages={chunk} />;
|
||||||
|
newPanels.push(panel);
|
||||||
|
positioneds.push({ position: panelId, panel: panel });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return Array.from(urls);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [linesIds, setLinesIds] = createSignal<string[]>([]);
|
setPanels(positioneds);
|
||||||
const [transportModeUrls] = createResource<string[], string[]>(linesIds, computeTransportModes);
|
|
||||||
|
|
||||||
createEffect(() => {
|
return newPanels;
|
||||||
setLinesIds(Object.keys(props.passages));
|
}}
|
||||||
});
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
export const PassagesDisplay: ParentComponent = () => {
|
||||||
<div class={styles.header}>
|
|
||||||
<Show when={transportModeUrls() !== undefined} >
|
|
||||||
<For each={transportModeUrls()}>
|
|
||||||
{(url) =>
|
|
||||||
<div class={styles.transportMode}>
|
|
||||||
<img src={url} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
<div class={styles.title}>
|
|
||||||
<svg viewBox="0 0 1260 50">
|
|
||||||
<text x="0" y="50%" dominant-baseline="middle" font-size="50" style="fill: #ffffff">
|
|
||||||
{props.title}
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class={styles.clock}>
|
|
||||||
<svg viewBox="0 0 115 43">
|
|
||||||
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-size="43" style="fill: #ffffff">
|
|
||||||
{format(dateNow(), "HH:mm")}
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div >
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Footer: VoidComponent<{ panels: PositionnedPanel[] }> = (props) => {
|
const MAX_PASSAGES_PER_PANEL = 5;
|
||||||
return (
|
|
||||||
<div class={styles.footer}>
|
// TODO: Use props.
|
||||||
<For each={props.panels}>
|
const syncPeriodMsec = 20 * 1000;
|
||||||
{(panel) => {
|
const panelSwitchPeriodMsec = 4 * 1000;
|
||||||
const position = panel.position;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<svg viewBox="0 0 29 29">
|
|
||||||
<circle cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3"
|
|
||||||
style={{ fill: `var(--idfm-${position == displayedPanelId() ? "white" : "black"})` }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.passagesDisplay}>
|
<div class={styles.passagesDisplay}>
|
||||||
<Header title="Prochains passages" passages={passages()} />
|
<PassagesDisplayProvider>
|
||||||
<div class={styles.panelsContainer}>
|
<Header title="Prochains passages" />
|
||||||
{() => {
|
<Body maxPassagesPerPanel={MAX_PASSAGES_PER_PANEL} syncPeriodMsec={syncPeriodMsec} panelSwitchPeriodMsec={panelSwitchPeriodMsec} />
|
||||||
setPanels([]);
|
<Footer />
|
||||||
|
</PassagesDisplayProvider>
|
||||||
let newPanels = [];
|
|
||||||
let positioneds: PositionnedPanel[] = [];
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
let chunk: Record<string, Record<string, Passage[]>> = {};
|
|
||||||
let chunkSize = 0;
|
|
||||||
|
|
||||||
console.log("passages=", passages());
|
|
||||||
for (const lineId of Object.keys(passages())) {
|
|
||||||
console.log("lineId=", lineId);
|
|
||||||
const byLinePassages = getLinePassages(lineId);
|
|
||||||
console.log("byLinePassages=", byLinePassages);
|
|
||||||
const byLinePassagesKeys = Object.keys(byLinePassages);
|
|
||||||
console.log("byLinePassagesKeys=", byLinePassagesKeys);
|
|
||||||
|
|
||||||
if (byLinePassagesKeys.length <= maxPassagePerPanel - chunkSize) {
|
|
||||||
chunk[lineId] = byLinePassages;
|
|
||||||
chunkSize += byLinePassagesKeys.length;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const panelid = index++;
|
|
||||||
const panel = <PassagesPanel show={panelid == displayedPanelId()} passages={chunk} />;
|
|
||||||
newPanels.push(panel);
|
|
||||||
positioneds.push({ position: panelid, panel: panel });
|
|
||||||
|
|
||||||
chunk = {};
|
|
||||||
chunk[lineId] = byLinePassages;
|
|
||||||
chunkSize = byLinePassagesKeys.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (chunkSize) {
|
|
||||||
const panelId = index++;
|
|
||||||
const panel = <PassagesPanel show={panelId == displayedPanelId()} passages={chunk} />;
|
|
||||||
newPanels.push(panel);
|
|
||||||
positioneds.push({ position: panelId, panel: panel });
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanels(positioneds);
|
|
||||||
return newPanels;
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<Footer panels={panels} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user