App directory refactoring

This commit is contained in:
Darren Clarke 2023-06-26 10:07:12 +00:00 committed by GitHub
parent a53a26f4c0
commit b312a8c862
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
153 changed files with 1532 additions and 1447 deletions

View file

@ -0,0 +1,31 @@
"use client";
import { FC } from "react";
import Link from "next/link";
import { Button as MUIButton } from "@mui/material";
interface ButtonProps {
text: string;
color: string;
href: string;
}
export const Button: FC<ButtonProps> = ({ text, color, href }) => (
<Link href={href} passHref>
<MUIButton
variant="contained"
disableElevation
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 999,
backgroundColor: color,
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
}}
>
{text}
</MUIButton>
</Link>
);

View file

@ -0,0 +1,22 @@
"use client";
import { FC } from "react";
import { Box, Grid } from "@mui/material";
type DisplayErrorProps = {
error: Error;
};
export const DisplayError: FC<DisplayErrorProps> = ({ error }) => (
<Grid
container
direction="column"
justifyContent="space-around"
alignItems="center"
style={{ height: 600, width: "100%", color: "red", fontSize: 20 }}
>
<Grid item>
<Box>{`Error: ${error.message}`}</Box>
</Grid>
</Grid>
);

View file

@ -0,0 +1,6 @@
"use client";
import { FC } from "react";
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
export const Home: FC = () => <ZammadWrapper path="/#dashboard" hideSidebar />;

View file

@ -0,0 +1,21 @@
"use client";
import { FC, PropsWithChildren, useState } from "react";
import { Grid } from "@mui/material";
import { Sidebar } from "./Sidebar";
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
const [open, setOpen] = useState(true);
return (
<Grid container direction="row">
<Sidebar open={open} setOpen={setOpen} />
<Grid
item
sx={{ ml: open ? "270px" : "100px", width: "100%", height: "100vh" }}
>
{children as any}
</Grid>
</Grid>
);
};

View file

@ -0,0 +1,57 @@
"use client";
import { FC, PropsWithChildren, useState } from "react";
import { CssBaseline } from "@mui/material";
import { CookiesProvider } from "react-cookie";
import { SessionProvider } from "next-auth/react";
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
import { SWRConfig } from "swr";
import { GraphQLClient } from "graphql-request";
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
const [csrfToken, setCsrfToken] = useState("");
const origin =
typeof window !== "undefined" && window.location.origin
? window.location.origin
: null;
const client = new GraphQLClient(`${origin}/proxy/zammad/graphql`, {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
const graphQLFetcher = async ({ document, variables }: any) => {
const requestHeaders = {
"X-CSRF-Token": csrfToken,
};
const { data, headers } = await client.rawRequest(
document,
variables,
requestHeaders
);
const token = headers.get("CSRF-Token");
setCsrfToken(token);
return data;
};
return (
<>
<CssBaseline />
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
<SWRConfig value={{ fetcher: graphQLFetcher }}>
<SessionProvider>
<CookiesProvider>
<LocalizationProvider dateAdapter={AdapterDateFns}>
{children}
</LocalizationProvider>
</CookiesProvider>
</SessionProvider>
</SWRConfig>
</NextAppDirEmotionCacheProvider>
</>
);
};

View file

@ -0,0 +1,554 @@
"use client";
import { FC, useState } from "react";
import useSWR from "swr";
import {
Box,
Grid,
Typography,
List,
ListItemButton,
ListItemIcon,
ListItemText,
ListItemSecondaryAction,
Drawer,
Collapse,
} from "@mui/material";
import {
FeaturedPlayList as FeaturedPlayListIcon,
Person as PersonIcon,
Analytics as AnalyticsIcon,
Logout as LogoutIcon,
Cottage as CottageIcon,
Settings as SettingsIcon,
ExpandCircleDown as ExpandCircleDownIcon,
Dvr as DvrIcon,
} from "@mui/icons-material";
import { usePathname } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import LinkLogo from "public/link-logo-small.png";
import { useSession, signOut } from "next-auth/react";
import { getTicketOverviewCountsQuery } from "@/app/_graphql/getTicketOverviewCountsQuery";
const openWidth = 270;
const closedWidth = 100;
const MenuItem = ({
name,
href,
Icon,
iconSize,
inset = false,
selected = false,
open = true,
badge,
target = "_self",
}: any) => (
<Link href={href} target={target}>
<ListItemButton
sx={{
p: 0,
mb: 1,
bl: iconSize === 0 ? "1px solid white" : "inherit",
}}
selected={selected}
>
{iconSize > 0 ? (
<ListItemIcon
sx={{
color: `white`,
minWidth: 0,
mr: 2,
textAlign: "center",
margin: open ? "0 8 0 0" : "0 auto",
}}
>
<Box
sx={{
width: iconSize,
height: iconSize,
mr: 0.5,
mt: "-4px",
}}
>
<Icon />
</Box>
</ListItemIcon>
) : (
<Box
sx={{
width: 30,
height: "28px",
position: "relative",
ml: "9px",
mr: "1px",
}}
>
<Box
sx={{
width: "1px",
height: "56px",
backgroundColor: "white",
position: "absolute",
left: "3px",
top: "-10px",
}}
/>
<Box
sx={{
width: "42px",
height: "42px",
position: "absolute",
top: "-27px",
left: "3px",
border: "solid 1px #fff",
borderColor: "transparent transparent transparent #fff",
borderRadius: "60px",
rotate: "-35deg",
}}
/>
</Box>
)}
{open && (
<ListItemText
inset={inset}
primary={
<Typography
variant="body1"
sx={{
fontSize: 16,
fontFamily: "Roboto",
fontWeight: "bold",
border: 0,
textAlign: "left",
color: "white",
}}
>
{name}
</Typography>
}
/>
)}
{badge && badge > 0 ? (
<ListItemSecondaryAction>
<Typography
color="textSecondary"
variant="body1"
className="badge"
sx={{
backgroundColor: "#FFB620",
color: "black !important",
borderRadius: 10,
px: 1,
fontSize: 12,
fontWeight: "bold",
}}
>
{badge}
</Typography>
</ListItemSecondaryAction>
) : null}
</ListItemButton>
</Link>
);
interface SidebarProps {
open: boolean;
setOpen: (open: boolean) => void;
}
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
const pathname = usePathname();
const { data: session } = useSession();
const username = session?.user?.name || "User";
const { data: overviewData, error: overviewError }: any = useSWR(
{
document: getTicketOverviewCountsQuery,
},
{ refreshInterval: 10000 }
);
const findOverviewCountByID = (id: number) =>
overviewData?.ticketOverviews?.edges?.find((overview: any) =>
overview.node.id.endsWith(`/${id}`)
)?.node?.ticketCount ?? 0;
const assignedCount = findOverviewCountByID(1);
const urgentCount = findOverviewCountByID(7);
const pendingCount = findOverviewCountByID(3);
const unassignedCount = findOverviewCountByID(2);
const logout = () => {
signOut({ callbackUrl: "/login" });
};
return (
<Drawer
sx={{ width: open ? openWidth : closedWidth, flexShrink: 0 }}
variant="permanent"
anchor="left"
open={open}
PaperProps={{
sx: {
width: open ? openWidth : closedWidth,
border: 0,
overflow: "visible",
},
}}
>
<Box
sx={{
position: "absolute",
top: 20,
right: open ? -8 : -16,
color: "#1C75FD",
rotate: open ? "90deg" : "-90deg",
}}
onClick={() => {
setOpen!(!open);
}}
>
<ExpandCircleDownIcon
sx={{
width: 30,
height: 30,
background: "white",
borderRadius: 500,
}}
/>
</Box>
<Grid
container
direction="column"
justifyContent="space-between"
wrap="nowrap"
sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }}
>
<Grid item container>
<Grid item sx={{ width: open ? "40px" : "100%" }}>
<Box
sx={{
width: "40px",
height: "40px",
margin: open ? "0" : "0 auto",
}}
>
<Image
src={LinkLogo}
alt="Link logo"
width={40}
height={40}
style={{
objectFit: "cover",
filter: "grayscale(100) brightness(100)",
}}
/>
</Box>
.
</Grid>
{open && (
<Grid item>
<Typography
variant="h2"
sx={{
fontSize: 26,
color: "white",
fontWeight: 700,
mt: 1,
ml: 0.5,
fontFamily: "Poppins",
}}
>
CDR Link
</Typography>
</Grid>
)}
</Grid>
<Grid item>
<Box
sx={{
height: "0.5px",
width: "100%",
backgroundColor: "#666",
mb: 1,
}}
/>
</Grid>
<Grid item>
<Typography
variant="h6"
sx={{
fontSize: 12,
color: "#999",
fontWeight: "bold",
textAlign: open ? "left" : "center",
}}
>
Hello
</Typography>
</Grid>
<Grid item>
<Typography
variant="h2"
sx={{
fontSize: 22,
color: "white",
mb: 1.5,
fontWeight: "bold",
textAlign: open ? "left" : "center",
}}
>
{open
? username
: username
.split(" ")
.map((name) => name.substring(0, 1))
.join("")}
</Typography>
</Grid>
<Grid item>
<Box
sx={{ height: "0.5px", width: "100%", backgroundColor: "#666" }}
/>
</Grid>
<Grid item container direction="column" sx={{ mt: "6px" }} flexGrow={1}>
<List
component="nav"
sx={{
a: {
textDecoration: "none",
".MuiListItemButton-root": {
p: 1,
borderRadius: 2,
"&:hover": {
background: "#555",
},
".MuiTypography-root": {
p: {
color: "#999 !important",
fontSize: 16,
},
},
".badge": {
p: { fontSize: 12, color: "black !important" },
},
},
".Mui-selected": {
background: "#444",
color: "#fff !important",
".MuiTypography-root": {
p: {
color: "#fff !important",
fontSize: 16,
},
},
".badge": {
p: { fontSize: 12, color: "black !important" },
},
},
},
}}
>
<MenuItem
name="Home"
href="/"
Icon={CottageIcon}
iconSize={20}
selected={pathname.endsWith("/")}
open={open}
/>
<MenuItem
name="Tickets"
href="/overview/assigned"
Icon={FeaturedPlayListIcon}
selected={
pathname.startsWith("/overview") ||
pathname.startsWith("/tickets")
}
iconSize={20}
open={open}
/>
<Collapse
in={
pathname.startsWith("/overview") ||
pathname.startsWith("/tickets")
}
timeout="auto"
unmountOnExit
onClick={undefined}
>
<List component="div" disablePadding>
<MenuItem
name="Assigned"
href="/overview/assigned"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/overview/assigned")}
badge={assignedCount}
open={open}
/>
<MenuItem
name="Urgent"
href="/overview/urgent"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/overview/urgent")}
badge={urgentCount}
open={open}
/>
<MenuItem
name="Pending"
href="/overview/pending"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/overview/pending")}
badge={pendingCount}
open={open}
/>
<MenuItem
name="Unassigned"
href="/overview/unassigned"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/overview/unassigned")}
badge={unassignedCount}
open={open}
/>
</List>
</Collapse>
<MenuItem
name="Knowledge Base"
href="/knowledge"
Icon={CottageIcon}
iconSize={20}
selected={pathname.endsWith("/knowledge")}
open={open}
/>
<MenuItem
name="Leafcutter"
href="/leafcutter/about"
Icon={AnalyticsIcon}
iconSize={20}
selected={pathname.endsWith("/leafcutter")}
open={open}
/>
<Collapse
in={pathname.startsWith("/leafcutter")}
timeout="auto"
unmountOnExit
onClick={undefined}
>
<List component="div" disablePadding>
{/*
<MenuItem
name="Dashboard"
href="/leafcutter"
iconSize={0}
selected={pathname.endsWith("/leafcutter")}
open={open}
/>
<MenuItem
name="Search and Create"
href="/leafcutter/create"
iconSize={0}
selected={pathname.endsWith("/leafcutter/create")}
open={open}
/>
<MenuItem
name="Trends"
href="/leafcutter/trends"
iconSize={0}
selected={pathname.endsWith("/leafcutter/trends")}
open={open}
/>
*/}
<MenuItem
name="FAQ"
href="/leafcutter/faq"
iconSize={0}
selected={pathname.endsWith("/leafcutter/faq")}
open={open}
/>
<MenuItem
name="About"
href="/leafcutter/about"
Icon={AnalyticsIcon}
iconSize={0}
selected={pathname.endsWith("/leafcutter/about")}
open={open}
/>
</List>
</Collapse>
<MenuItem
name="Profile"
href="/profile"
Icon={PersonIcon}
iconSize={20}
selected={pathname.endsWith("/profile")}
open={open}
/>
<MenuItem
name="Admin"
href="/admin/zammad"
Icon={SettingsIcon}
iconSize={20}
open={open}
/>
<Collapse
in={pathname.startsWith("/admin/")}
timeout="auto"
unmountOnExit
onClick={undefined}
>
<List component="div" disablePadding>
<MenuItem
name="Zammad Settings"
href="/admin/zammad"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/zammad")}
open={open}
/>
<MenuItem
name="Metamigo"
href="/admin/metamigo"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/metamigo")}
open={open}
/>
<MenuItem
name="Label Studio"
href="/admin/label-studio"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/label-studio")}
open={open}
/>
</List>
</Collapse>
<MenuItem
name="Zammad Interface"
href="/proxy/zammad"
Icon={DvrIcon}
iconSize={20}
open={open}
target="_blank"
/>
<MenuItem
name="Logout"
href="/logout"
Icon={LogoutIcon}
iconSize={20}
open={open}
onClick={logout}
/>
</List>
</Grid>
</Grid>
</Drawer>
);
};

View file

@ -0,0 +1,86 @@
"use client";
import { FC } from "react";
import { Box } from "@mui/material";
import {
DataGridPro,
GridColDef,
GridColumnVisibilityModel,
GridRowSelectionModel,
} from "@mui/x-data-grid-pro";
import { useCookies } from "react-cookie";
interface StyledDataGridProps {
name: string;
columns: GridColDef[];
rows: any[];
// eslint-disable-next-line no-unused-vars
onRowClick?: (row: any) => void;
height?: string;
selectedRows?: GridRowSelectionModel;
// eslint-disable-next-line no-unused-vars
setSelectedRows?: (rows: GridRowSelectionModel) => void;
}
export const StyledDataGrid: FC<StyledDataGridProps> = ({
name,
columns,
rows,
onRowClick,
height = "calc(100vh - 20px)",
selectedRows,
setSelectedRows,
}) => {
const cookieName = `${name}DataGridColumnState`;
const [cookies, setCookie] = useCookies([cookieName]);
const handleColumnVisibilityChange = (model: GridColumnVisibilityModel) =>
setCookie(cookieName, model, { path: "/" });
return (
<Box
sx={{
backgroundColor: "#ddd",
border: 0,
width: "100%",
height,
".MuiDataGrid-row:nth-of-type(1n)": {
backgroundColor: "#f3f3f3",
},
".MuiDataGrid-row:nth-of-type(2n)": {
backgroundColor: "#fff",
},
".MuiDataGrid-columnHeaderTitle": {
color: "#333",
fontWeight: "bold",
fontSize: 11,
margin: "0 auto",
},
".MuiDataGrid-columnHeader": {
backgroundColor: "#ccc",
border: 0,
borderBottom: "3px solid #ddd",
},
}}
>
<DataGridPro
rows={rows}
columns={columns}
density="compact"
hideFooter
sx={{ height }}
rowBuffer={30}
checkboxSelection={!!setSelectedRows}
onRowSelectionModelChange={setSelectedRows}
rowSelectionModel={selectedRows}
rowHeight={46}
scrollbarSize={0}
disableVirtualization
columnVisibilityModel={
(cookies[cookieName] as GridColumnVisibilityModel) ?? undefined
}
onColumnVisibilityModelChange={handleColumnVisibilityChange}
onRowClick={onRowClick}
/>
</Box>
);
};

View file

@ -0,0 +1,88 @@
"use client";
import { FC, useState } from "react";
import getConfig from "next/config";
import { useRouter } from "next/navigation";
import Iframe from "react-iframe";
type ZammadWrapperProps = {
path: string;
hideSidebar?: boolean;
};
export const ZammadWrapper: FC<ZammadWrapperProps> = ({
path,
hideSidebar = true,
}) => {
const router = useRouter();
const [display, setDisplay] = useState("inherit");
//const {
// publicRuntimeConfig: { linkURL },
// } = getConfig();
const linkURL = "http://localhost:3000";
const url = `${linkURL}/proxy/zammad${path}`;
console.log({ url });
return (
// @ts-ignore
<Iframe
id="zammad"
url={url}
width="100%"
height="100%"
frameBorder={0}
styles={{ display }}
onLoad={() => {
const linkElement = document.querySelector("iframe");
// const baseElement = linkElement.contentDocument.createElement("base");
// baseElement.href = `${linkURL}/proxy/zammad`;
if (
linkElement.contentDocument &&
linkElement.contentDocument?.querySelector &&
linkElement.contentDocument.querySelector("#navigation") &&
linkElement.contentDocument.querySelector("body") &&
linkElement.contentDocument.querySelector(".sidebar")
) {
// @ts-ignore
linkElement.contentDocument.querySelector("#navigation").style =
"display: none";
// @ts-ignore
linkElement.contentDocument.querySelector("body").style =
"font-family: Arial";
if (hideSidebar) {
// @ts-ignore
linkElement.contentDocument.querySelector(".sidebar").style =
"display: none";
}
// @ts-ignore
if (linkElement.contentDocument.querySelector(".overview-header")) {
// @ts-ignore
(
linkElement.contentDocument.querySelector(
".overview-header"
) as any
).style = "display: none";
}
setDisplay("inherit");
if (linkElement.contentWindow) {
linkElement.contentWindow.addEventListener("hashchange", () => {
const hash = linkElement.contentWindow?.location?.hash ?? "";
if (hash.startsWith("#ticket/zoom/")) {
setDisplay("none");
const ticketID = hash.split("/").pop();
router.push(`/tickets/${ticketID}`);
setTimeout(() => {
setDisplay("inherit");
}, 1000);
}
});
}
}
}}
/>
);
};

View file

@ -0,0 +1,19 @@
import { gql } from 'graphql-request';
export const getTicketOverviewCountsQuery = gql`
query ticketOverviewTicketCount {
ticketOverviews {
edges {
node {
id
name
ticketCount
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}`;

View file

@ -0,0 +1,40 @@
import { gql } from 'graphql-request';
export const getTicketQuery = gql`
query getTicket($ticketId: ID!) {
ticket(ticket: { ticketId: $ticketId }) {
id
internalId
title
note
number
createdAt
updatedAt
closeAt
tags
state {
id
name
}
owner {
id
email
}
articles {
edges {
node {
id
body
internal
type {
name
}
sender {
name
}
}
}
}
}
}
`;

View file

@ -0,0 +1,60 @@
import { gql } from 'graphql-request';
export const getTicketsByOverviewQuery = gql`
query ticketsByOverview($overviewId: ID!, $orderBy: String, $orderDirection: EnumOrderDirection, $cursor: String, $showPriority: Boolean = false, $showUpdatedBy: Boolean = false, $pageSize: Int = 10) {
ticketsByOverview(
overviewId: $overviewId
orderBy: $orderBy
orderDirection: $orderDirection
after: $cursor
first: $pageSize
) {
totalCount
edges {
node {
id
internalId
number
title
createdAt
updatedAt
updatedBy @include(if: $showUpdatedBy) {
id
fullname
}
customer {
id
firstname
lastname
fullname
}
organization {
id
name
}
state {
id
name
stateType {
name
}
}
group {
id
name
}
priority @include(if: $showPriority) {
id
name
uiColor
defaultCreate
}
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}`;

View file

@ -0,0 +1,10 @@
import { gql } from "graphql-request";
export const updateTicketMutation = gql`
mutation UpdateTicket($ticketId: ID!, $input: TicketUpdateInput!) {
ticketUpdate(ticketId: $ticketId, input: $input) {
ticket {
id
}
}
}`;

View file

@ -0,0 +1,94 @@
body {
overscroll-behavior-x: none;
overscroll-behavior-y: none;
text-size-adjust: none;
}
.internal-note .cs-message__content {
background-color: #FFB62088 !important;
border: 2px solid #FFB620 !important;
border-radius: 0 !important;
margin: 12px;
font-family: Roboto !important;
font-size: 16px !important;
padding: 20px !important;
width: 300px;
}
.incoming-message .cs-message__content {
color: black !important;
background-color: #ddd !important;
border-radius: 14px !important;
margin: 12px;
font-family: Roboto !important;
font-size: 16px !important;
padding: 20px !important;
}
.outgoing-message {
margin-left: calc(100% - 550px) !important;
margin-right: 0 !important;
}
.internal-note {
margin-left: calc(100% - 350px) !important;
margin-right: 0 !important;
}
.outgoing-message .cs-message__content {
color: white !important;
background-color: #1982FC !important;
border-radius: 14px !important;
margin: 12px;
font-family: Roboto !important;
font-size: 16px !important;
padding: 20px !important;
width: 500px;
}
.incoming-message .cs-message__content a {
color: white !important;
}
.outgoing-message .cs-message__content a {
color: white !important;
}
.cs-message-input__content-editor-wrapper {
background-color: white !important;
border: 1px solid #ccc !important;
margin: 10px !important;
}
.cs-message-input__content-editor-container {
background-color: white !important;
}
.cs-message-input__content-editor {
background-color: white !important;
font-family: Roboto !important;
}
.cs-conversation-header {
background-color: #ddd !important;
border: 0 !important;
padding: 20px !important;
border-bottom: 1px solid #ccc !important;
}
.cs-message-list {
background-color: #eee !important;
}
.cs-message-input {
background-color: #dfdfdf !important;
}
.cs-main-container {
border: 0 !important;
border-right: 1px solid #ccc !important;
}

View file

@ -0,0 +1,86 @@
export const colors: any = {
lightGray: "#ededf0",
mediumGray: "#e3e5e5",
darkGray: "#33302f",
mediumBlue: "#4285f4",
green: "#349d7b",
lavender: "#a5a6f6",
darkLavender: "#5d5fef",
pink: "#fcddec",
cdrLinkOrange: "#ff7115",
coreYellow: "#fac942",
helpYellow: "#fff4d5",
dwcDarkBlue: "#191847",
hazyMint: "#ecf7f8",
leafcutterElectricBlue: "#4d6aff",
leafcutterLightBlue: "#fafbfd",
waterbearElectricPurple: "#332c83",
waterbearLightSmokePurple: "#eff3f8",
bumpedPurple: "#212058",
mutedPurple: "#373669",
warningPink: "#ef5da8",
lightPink: "#fff0f7",
lightGreen: "#f0fff3",
lightOrange: "#fff5f0",
beige: "#f6f2f1",
almostBlack: "#33302f",
white: "#ffffff",
};
export const typography: any = {
h1: {
fontFamily: "Playfair, serif",
fontSize: 45,
fontWeight: 700,
lineHeight: 1.1,
margin: 0,
},
h2: {
fontFamily: "Poppins, sans-serif",
fontSize: 35,
fontWeight: 700,
lineHeight: 1.1,
margin: 0,
},
h3: {
fontFamily: "Poppins, sans-serif",
fontWeight: 400,
fontSize: 27,
lineHeight: 1.1,
margin: 0,
},
h4: {
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
fontSize: 18,
},
h5: {
fontFamily: "Roboto, sans-serif",
fontWeight: 700,
fontSize: 16,
lineHeight: "24px",
textTransform: "uppercase",
textAlign: "center",
margin: 1,
},
h6: {
fontFamily: "Roboto, sans-serif",
fontWeight: 400,
fontSize: 14,
textAlign: "center",
},
p: {
fontFamily: "Roboto, sans-serif",
fontSize: 17,
lineHeight: "26.35px",
fontWeight: 400,
margin: 0,
},
small: {
fontFamily: "Roboto, sans-serif",
fontSize: 13,
lineHeight: "18px",
fontWeight: 400,
margin: 0,
},
};

View file

@ -0,0 +1,22 @@
import { FC } from "react";
import { Grid } from "@mui/material";
import Iframe from "react-iframe";
export const LabelStudioWrapper: FC = () => (
<Grid
container
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid item sx={{ height: "100vh", width: "100%" }}>
<Iframe
id="link"
url={"https://label-studio:3000"}
width="100%"
height="100%"
frameBorder={0}
/>
</Grid>
</Grid>
);

View file

@ -0,0 +1,10 @@
import { Metadata } from "next";
import { LabelStudioWrapper } from "./_components/LabelStudioWrapper";
export const metadata: Metadata = {
title: "Label Studio",
};
export default function Page() {
return <LabelStudioWrapper />;
}

View file

@ -0,0 +1,36 @@
"use client";
import { FC } from "react";
import getConfig from "next/config";
import { Grid } from "@mui/material";
import Iframe from "react-iframe";
type MetamigoWrapperProps = {
path: string;
};
export const MetamigoWrapper: FC<MetamigoWrapperProps> = ({ path }) => {
const {
publicRuntimeConfig: { linkURL },
} = getConfig();
const fullMetamigoURL = `${linkURL}/metamigo/${path}`;
return (
<Grid
container
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid item sx={{ height: "100vh", width: "100%" }}>
<Iframe
id="link"
url={fullMetamigoURL}
width="100%"
height="100%"
frameBorder={0}
/>
</Grid>
</Grid>
);
};

View file

@ -0,0 +1,16 @@
import { Metadata } from "next";
import { MetamigoWrapper } from "./_components/MetamigoWrapper";
export const metadata: Metadata = {
title: "Metamigo",
};
type PageProps = {
params: {
path: string;
};
};
export default function Page({ params: { path } }: PageProps) {
return <MetamigoWrapper path={path} />;
}

View file

@ -0,0 +1,10 @@
import { Metadata } from "next";
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
export const metadata: Metadata = {
title: "Zammad",
};
export default function Page() {
return <ZammadWrapper path="/#manage" hideSidebar={false} />;
}

View file

@ -0,0 +1,22 @@
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
// import Apple from "next-auth/providers/apple";
const handler = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
/*
Apple({
clientId: process.env.APPLE_CLIENT_ID,
clientSecret: process.env.APPLE_CLIENT_SECRET
}),
*/
],
secret: process.env.NEXTAUTH_SECRET,
});
export { handler as GET, handler as POST };

View file

@ -0,0 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
const handler = (req: NextRequest) => {
NextResponse.redirect('/proxy/zammad/api/v1' + req.url.substring('/api/v1'.length));
};
export { handler as GET, handler as POST };

11
apps/link/app/error.tsx Normal file
View file

@ -0,0 +1,11 @@
"use client";
import { DisplayError } from "./_components/DisplayError";
type PageProps = {
error: Error;
};
export default function Page({ error }: PageProps) {
return <DisplayError error={error} />;
}

View file

@ -0,0 +1,10 @@
import { Metadata } from "next";
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
export const metadata: Metadata = {
title: "Knowledge Base",
};
export default function Page() {
return <ZammadWrapper path="/#knowledge_base/1/locale/en-us" />;
}

35
apps/link/app/layout.tsx Normal file
View file

@ -0,0 +1,35 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import "./_styles/global.css";
import "@fontsource/poppins/400.css";
import "@fontsource/poppins/700.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css";
// import getConfig from "next/config";
// import { LicenseInfo } from "@mui/x-data-grid-pro";
import { MultiProvider } from "./_components/MultiProvider";
import { InternalLayout } from "./_components/InternalLayout";
export const metadata: Metadata = {
title: "Link",
};
type LayoutProps = {
children: ReactNode;
};
export default function Layout({ children }: LayoutProps) {
// const { publicRuntimeConfig } = getConfig();
// LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey);
return (
<html lang="en">
<body>
<MultiProvider>
<InternalLayout>{children}</InternalLayout>
</MultiProvider>
</body>
</html>
);
}

View file

@ -0,0 +1,36 @@
"use client";
import { FC } from "react";
import getConfig from "next/config";
import { Grid } from "@mui/material";
import Iframe from "react-iframe";
type LeafcutterWrapperProps = {
path: string;
};
export const LeafcutterWrapper: FC<LeafcutterWrapperProps> = ({ path }) => {
const {
publicRuntimeConfig: { linkURL },
} = getConfig();
const fullLeafcutterURL = `${linkURL}/proxy/leafcutter/${path}`;
return (
<Grid
container
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid item sx={{ height: "100vh", width: "100%" }}>
<Iframe
id="leafcutter"
url={fullLeafcutterURL}
width="100%"
height="100%"
frameBorder={0}
/>
</Grid>
</Grid>
);
};

View file

@ -0,0 +1,11 @@
import { LeafcutterWrapper } from "./_components/LeafcutterWrapper";
type PageProps = {
params: {
view: string;
};
};
export default function Page({ params: { view } }: PageProps) {
<LeafcutterWrapper path={view} />;
}

View file

@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function Page() {
redirect("/leafcutter/home");
}

View file

@ -0,0 +1,84 @@
"use client";
import { FC } from "react";
import { Box, Grid, Container, IconButton } from "@mui/material";
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
import { signIn } from "next-auth/react";
type LoginProps = {
session: any;
};
export const Login: FC<LoginProps> = ({ session }) => {
const origin =
typeof window !== "undefined" && window.location.origin
? window.location.origin
: "";
const buttonStyles = {
borderRadius: 500,
width: "100%",
fontSize: "16px",
fontWeight: "bold",
};
return (
<>
<Grid container direction="row-reverse" sx={{ p: 3 }}>
<Grid item />
</Grid>
<Container maxWidth="md" sx={{ mt: 3, mb: 20 }}>
<Grid container spacing={2} direction="column" alignItems="center">
<Grid item>
<Box sx={{ maxWidth: 200 }} />
</Grid>
<Grid item sx={{ textAlign: "center" }} />
<Grid item>
{!session ? (
<Grid
container
spacing={3}
direction="column"
alignItems="center"
sx={{ width: 450, mt: 1 }}
>
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("google", {
callbackUrl: `${origin}/proxy/zammad/auth/sso`,
})
}
>
<GoogleIcon sx={{ mr: 1 }} />
Google
</IconButton>
</Grid>
{/*
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("apple", {
callbackUrl: `${window.location.origin}/setup`,
})
}
>
<AppleIcon sx={{ mr: 1 }} />
</IconButton>
</Grid>*/}
<Grid item sx={{ mt: 2 }} />
</Grid>
) : null}
{session ? (
<Box component="h4">
{` ${session.user.name ?? session.user.email}.`}
</Box>
) : null}
</Grid>
</Grid>
</Container>
</>
);
};

View file

@ -0,0 +1,12 @@
import { Metadata } from "next";
import { getSession } from "next-auth/react";
import { Login } from "./_components/Login";
export const metadata: Metadata = {
title: "Login",
};
export default async function Page() {
const session = await getSession();
return <Login session={session} />;
}

View file

@ -0,0 +1,73 @@
"use client";
import { FC } from "react";
import { Grid, Box } from "@mui/material";
import { GridColDef } from "@mui/x-data-grid-pro";
import { StyledDataGrid } from "../../../_components/StyledDataGrid";
import { typography } from "@/app/_styles/theme";
import { useRouter } from "next/navigation";
interface TicketListProps {
title: string;
tickets: any;
}
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
const router = useRouter();
let gridColumns: GridColDef[] = [
{
field: "number",
headerName: "Number",
flex: 1,
},
{
field: "title",
headerName: "Title",
flex: 1,
},
{
field: "customer",
headerName: "Sender",
valueGetter: (params) => params.row?.customer?.fullname,
flex: 1,
},
{
field: "group",
headerName: "Group",
valueGetter: (params) => params.row?.group?.name,
flex: 1,
},
];
console.log({ tickets });
const rowClick = ({ row }) => {
router.push(`/tickets/${row.internalId}`);
};
return (
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
<Grid container direction="column">
<Grid item>
<Box
sx={{
backgroundColor: "#ddd",
px: "8px",
pb: "16px",
...typography.h4,
fontSize: 24,
}}
>
{title}
</Box>
</Grid>
<Grid item>
<StyledDataGrid
name={title}
columns={gridColumns}
rows={tickets}
onRowClick={rowClick}
/>
</Grid>
</Grid>
</Box>
);
};

View file

@ -0,0 +1,32 @@
"use client";
import { FC } from "react";
import useSWR from "swr";
import { TicketList } from "./TicketList";
import { getTicketsByOverviewQuery } from "@/app/_graphql/getTicketsByOverviewQuery";
type ZammadOverviewProps = {
name: string;
id: string;
};
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name, id }) => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketsByOverviewQuery,
variables: { overviewId: `gid://zammad/Overview/${id}` },
},
{ refreshInterval: 10000 }
);
const shouldRender = !ticketError && ticketData;
const tickets =
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
return (
<>
{shouldRender && <TicketList title="Assigned" tickets={tickets} />}
{ticketError && <div>{ticketError.toString()}</div>}
</>
);
};

View file

@ -0,0 +1,11 @@
"use client";
import { DisplayError } from "app/_components/DisplayError";
type PageProps = {
error: Error;
};
export default function Page({ error }: PageProps) {
return <DisplayError error={error} />;
}

View file

@ -0,0 +1,36 @@
import { Metadata } from "next";
import { ZammadOverview } from "./_components/ZammadOverview";
type MetadataProps = {
params: {
overview: string;
};
};
export async function generateMetadata({
params: { overview },
}: MetadataProps): Promise<Metadata> {
const section = overview.charAt(0).toUpperCase() + overview.slice(1);
return {
title: `Link - ${section} Tickets`,
};
}
const overviews = {
assigned: 1,
unassigned: 2,
pending: 3,
urgent: 7,
};
type PageProps = {
params: {
overview: string;
};
};
export default function Page({ params: { overview } }: PageProps) {
console.log({ overview });
return <ZammadOverview name={overview} id={overviews[overview]} />;
}

10
apps/link/app/page.tsx Normal file
View file

@ -0,0 +1,10 @@
import { Metadata } from "next";
import { Home } from "@/app/_components/Home";
export const metadata: Metadata = {
title: "Link",
};
export default function Page() {
return <Home />;
}

View file

@ -0,0 +1,10 @@
import { Metadata } from "next";
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
export const metadata: Metadata = {
title: "Profile",
};
export default function Page() {
return <ZammadWrapper path="/#profile" hideSidebar={false} />;
}

View file

@ -0,0 +1,102 @@
"use client";
import { FC, useState } from "react";
import {
Grid,
Button,
Dialog,
DialogActions,
DialogContent,
TextField,
} from "@mui/material";
import { useSWRConfig } from "swr";
import { updateTicketMutation } from "@/app/_graphql/updateTicketMutation";
interface ArticleCreateDialogProps {
ticketID: string;
open: boolean;
closeDialog: () => void;
kind: "reply" | "note";
}
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
ticketID,
open,
closeDialog,
kind,
}) => {
const [body, setBody] = useState("");
const backgroundColor = kind === "reply" ? "#1982FC" : "#FFB620";
const color = kind === "reply" ? "white" : "black";
const { fetcher } = useSWRConfig();
const createArticle = async () => {
await fetcher({
document: updateTicketMutation,
variables: {
ticketId: `gid://zammad/Ticket/${ticketID}`,
input: {
article: {
body,
type: kind === "note" ? "note" : "phone",
internal: kind === "note",
},
},
},
});
closeDialog();
setBody("");
};
return (
<Dialog open={open} maxWidth="sm" fullWidth>
<DialogContent>
<TextField
label={kind === "reply" ? "Write reply" : "Write internal note"}
multiline
rows={10}
fullWidth
value={body}
onChange={(e: any) => setBody(e.target.value)}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pt: 0, pb: 3 }}>
<Grid container justifyContent="space-between">
<Grid item>
<Button
sx={{
backgroundColor: "white",
color: "#666",
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
}}
onClick={() => {
setBody("");
closeDialog();
}}
>
Cancel
</Button>
</Grid>
<Grid item>
<Button
sx={{
backgroundColor,
color,
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
px: 3,
}}
onClick={createArticle}
>
{kind === "reply" ? "Send Reply" : "Save Note"}
</Button>
</Grid>
</Grid>
</DialogActions>
</Dialog>
);
};

View file

@ -0,0 +1,177 @@
"use client";
import { FC, useState } from "react";
import useSWR from "swr";
import { getTicketQuery } from "@/app/_graphql/getTicketQuery";
import {
Grid,
Box,
Typography,
Button,
Dialog,
DialogActions,
DialogContent,
} from "@mui/material";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import {
MainContainer,
ChatContainer,
MessageList,
Message,
ConversationHeader,
} from "@chatscope/chat-ui-kit-react";
import { ArticleCreateDialog } from "./ArticleCreateDialog";
interface TicketDetailProps {
id: number;
}
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 100000 }
);
console.log({ ticketData, ticketError });
const ticket = ticketData?.ticket;
const [dialogOpen, setDialogOpen] = useState(false);
const [articleKind, setArticleKind] = useState<"reply" | "note">("reply");
const closeDialog = () => setDialogOpen(false);
const shouldRender = ticketData && !ticketError;
return (
shouldRender && (
<>
<MainContainer>
<ChatContainer>
<ConversationHeader>
<ConversationHeader.Content>
<Box
sx={{
width: "100%",
textAlign: "center",
fontWeight: "bold",
}}
>
<Typography
variant="h5"
sx={{ fontFamily: "Poppins", fontWeight: 700 }}
>
{ticket.title}
</Typography>
<Typography
variant="h6"
sx={{ fontFamily: "Roboto", fontWeight: 400 }}
>{`Ticket #${ticket.number} (created ${new Date(
ticket.createdAt
).toLocaleDateString()})`}</Typography>
</Box>
</ConversationHeader.Content>
</ConversationHeader>
<MessageList style={{ marginBottom: 80 }}>
{ticket.articles.edges.map(({ node: article }: any) => (
<Message
key={article.id}
className={
article.internal
? "internal-note"
: article.sender.name === "Agent"
? "outgoing-message"
: "incoming-message"
}
model={{
message: article.body.replace(/<div>*<br>*<div>/g, ""),
sentTime: article.updated_at,
sender: article.from,
direction:
article.sender === "Agent" ? "outgoing" : "incoming",
position: "single",
}}
/>
))}
</MessageList>
</ChatContainer>
<Box
sx={{
height: 80,
background: "#eeeeee",
borderTop: "1px solid #ddd",
position: "absolute",
bottom: 0,
width: "100%",
zIndex: 1000,
}}
>
<Grid
container
spacing={4}
justifyContent="center"
alignItems="center"
alignContent="center"
>
<Grid item>
<Button
variant="contained"
disableElevation
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
backgroundColor: "#1982FC",
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
py: "10px",
mt: 2,
}}
onClick={() => {
setArticleKind("reply");
setDialogOpen(true);
}}
>
Reply to ticket
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
disableElevation
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
color: "black",
backgroundColor: "#FFB620",
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
py: "10px",
mt: 2,
}}
onClick={() => {
setArticleKind("note");
setDialogOpen(true);
}}
>
Write note to agent
</Button>
</Grid>
</Grid>
</Box>
</MainContainer>
<ArticleCreateDialog
ticketID={ticket.internalId}
open={dialogOpen}
closeDialog={closeDialog}
kind={articleKind}
/>
</>
)
);
};

View file

@ -0,0 +1,11 @@
import { TicketDetail } from "./_components/TicketDetail";
type PageProps = {
params: {
id: string;
};
};
export default function Page({ params: { id } }: PageProps) {
return <TicketDetail id={id} />;
}

View file

@ -0,0 +1,167 @@
"use client";
import { FC, useEffect, useState } from "react";
import {
Grid,
Box,
Typography,
TextField,
Stack,
Chip,
Select,
MenuItem,
} from "@mui/material";
import useSWR, { useSWRConfig } from "swr";
import { updateTicketMutation } from "@/app/_graphql/updateTicketMutation";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
interface TicketEditProps {
ticket: any;
}
export const TicketEdit: FC<TicketEditProps> = ({ ticket }) => {
const [selectedGroup, setSelectedGroup] = useState(1);
const [selectedOwner, setSelectedOwner] = useState(1);
const [selectedPriority, setSelectedPriority] = useState(1);
const [selectedState, setSelectedState] = useState(1);
const [selectedTags, setSelectedTags] = useState(["tag1", "tag2"]);
const handleDelete = () => {
console.info("You clicked the delete icon.");
};
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
/*
const { data: groups } = useSWR("/api/v1/groups", restFetcher);
console.log({ groups });
const { data: users } = useSWR("/api/v1/users", restFetcher);
console.log({ users });
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
console.log({ states });
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
console.log({ priorities });
*/
const groups = [];
const users = [];
const states = [];
const priorities = [];
const { fetcher } = useSWRConfig();
const updateTicket = async () => {
await fetcher({
document: updateTicketMutation,
variables: {
ticketId: ticket.id,
input: {
ownerId: `gid://zammad/User/${selectedOwner}`,
tags: ["tag1", "tag2"],
},
},
});
};
return (
<Box sx={{ height: "100vh", background: "#ddd", p: 2 }}>
<Grid container direction="column" spacing={3}>
<Grid item>
<Box sx={{ m: 1 }}>Group</Box>
<Select
defaultValue={selectedGroup}
value={selectedGroup}
onChange={(e: any) => {
setSelectedGroup(e.target.value);
updateTicket();
}}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{groups?.map((group: any) => (
<MenuItem key={group.id} value={group.id}>
{group.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>Owner</Box>
<Select
value={selectedOwner}
onChange={(e: any) => {
setSelectedOwner(e.target.value);
updateTicket();
}}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{users?.map((user: any) => (
<MenuItem key={user.id} value={user.id}>
{user.firstname} {user.lastname}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>State</Box>
<Select
value={selectedState}
onChange={(e: any) => setSelectedState(e.target.value)}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{states?.map((state: any) => (
<MenuItem key={state.id} value={state.id}>
{state.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
<Select
value={selectedPriority}
onChange={(e: any) => setSelectedPriority(e.target.value)}
size="small"
sx={{
width: "100%",
backgroundColor: "white",
}}
>
{priorities?.map((priority: any) => (
<MenuItem key={priority.id} value={priority.id}>
{priority.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Box sx={{ mb: 1 }}>Tags</Box>
<Stack
direction="row"
spacing={1}
sx={{
backgroundColor: "white",
p: 1,
borderRadius: "6px",
border: "1px solid #bbb",
minHeight: 120,
}}
flexWrap="wrap"
>
{selectedTags.map((tag: string) => (
<Chip key={tag} label={tag} onDelete={handleDelete} />
))}
</Stack>
</Grid>
</Grid>
</Box>
);
};

View file

@ -0,0 +1,5 @@
import { TicketEdit } from "./_components/TicketEdit";
export default function Page() {
return <TicketEdit ticket={undefined} />;
}

View file

@ -0,0 +1,11 @@
"use client";
import { DisplayError } from "app/_components/DisplayError";
type PageProps = {
error: Error;
};
export default function Page({ error }: PageProps) {
return <DisplayError error={error} />;
}

View file

@ -0,0 +1,25 @@
"use client";
import { ReactNode } from "react";
import { Grid } from "@mui/material";
type LayoutProps = {
detail: any;
edit: any;
params: {
id: string;
};
};
export default function Layout({ detail, edit, params: { id } }: LayoutProps) {
return (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
<Grid item sx={{ height: "100vh" }} xs={9}>
{detail}
</Grid>
<Grid item xs={3} sx={{ height: "100vh" }}>
{edit}
</Grid>
</Grid>
);
}

View file

@ -0,0 +1,15 @@
"use client";
import Link from "next/link";
export default function Page() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<p>
View <Link href="/blog">all posts</Link>
</p>
</div>
);
}

View file

@ -0,0 +1,62 @@
type PageProps = {
params: {
id: string;
};
};
export default function Page({ params: { id } }: PageProps) {
return <div>Page</div>;
}
/*
import { GetServerSideProps, GetServerSidePropsContext } from "next";
import Head from "next/head";
import useSWR from "swr";
import { NextPage } from "next";
import { Grid } from "@mui/material";
import { TicketDetail } from "@/app/_components/TicketDetail";
import { TicketEdit } from "@/app/_components/TicketEdit";
import { getTicketQuery } from "@/app/_graphql/getTicketQuery";
type TicketProps = {
id: string;
};
const Ticket: NextPage<TicketProps> = ({ id }) => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 100000 }
);
const shouldRender = !ticketError && ticketData;
return (
<>
{shouldRender && (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
<Grid item sx={{ height: "100vh" }} xs={9}>
<TicketDetail ticket={ticketData.ticket} />
</Grid>
<Grid item xs={3} sx={{ height: "100vh" }}>
<TicketEdit ticket={ticketData.ticket} />
</Grid>
</Grid>
)}
{ticketError && <div>{ticketError.toString()}</div>}
</>
);
};
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { id } = context.query;
return { props: { id } };
};
export default Ticket;
*/