projects
do main
1 rok temu
@@ -1,8 +1,9 @@ | |||
{ | |||
"extends": ["next/core-web-vitals", "prettier"], | |||
"plugins": ["prettier"], | |||
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended", "prettier"], | |||
"plugins": ["prettier", "@typescript-eslint"], | |||
"rules": { | |||
"prettier/prettier": "warn", | |||
"no-unused-vars": "warn" | |||
"no-unused-vars": "off", | |||
"@typescript-eslint/no-unused-vars": "warn" | |||
} | |||
} |
@@ -23,6 +23,7 @@ | |||
"dayjs": "^1.11.10", | |||
"i18next": "^23.7.11", | |||
"i18next-resources-to-backend": "^1.2.0", | |||
"lodash": "^4.17.21", | |||
"next": "14.0.4", | |||
"next-auth": "^4.24.5", | |||
"react": "^18", | |||
@@ -32,9 +33,12 @@ | |||
"react-intl": "^6.5.5" | |||
}, | |||
"devDependencies": { | |||
"@types/lodash": "^4.14.202", | |||
"@types/node": "^20", | |||
"@types/react": "^18", | |||
"@types/react-dom": "^18", | |||
"@typescript-eslint/eslint-plugin": "^6.18.1", | |||
"@typescript-eslint/parser": "^6.18.1", | |||
"autoprefixer": "^10.4.16", | |||
"eslint": "^8", | |||
"eslint-config-next": "14.0.4", | |||
@@ -4,6 +4,8 @@ import { authOptions } from "@/config/authConfig"; | |||
import { redirect } from "next/navigation"; | |||
import Box from "@mui/material/Box"; | |||
import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | |||
import Stack from "@mui/material/Stack"; | |||
import Breadcrumb from "@/components/Breadcrumb"; | |||
export default async function MainLayout({ | |||
children, | |||
@@ -26,9 +28,13 @@ export default async function MainLayout({ | |||
component="main" | |||
sx={{ | |||
marginInlineStart: { xs: 0, lg: NAVIGATION_CONTENT_WIDTH }, | |||
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | |||
}} | |||
> | |||
{children} | |||
<Stack spacing={2}> | |||
<Breadcrumb /> | |||
{children} | |||
</Stack> | |||
</Box> | |||
</> | |||
); | |||
@@ -0,0 +1,21 @@ | |||
import CreateProject from "@/components/CreateProject"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
export const metadata: Metadata = { | |||
title: "Create Project", | |||
}; | |||
const Projects: React.FC = async () => { | |||
const { t } = await getServerI18n("projects"); | |||
return ( | |||
<> | |||
<Typography variant="h4">{t("Create Project")}</Typography> | |||
<CreateProject /> | |||
</> | |||
); | |||
}; | |||
export default Projects; |
@@ -1,11 +1,47 @@ | |||
import { preloadProjects } from "@/app/api/projects"; | |||
import ProjectSearch from "@/components/ProjectSearch"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Add from "@mui/icons-material/Add"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
import Link from "next/link"; | |||
import { Suspense } from "react"; | |||
export const metadata: Metadata = { | |||
title: "Projects", | |||
}; | |||
const Projects: React.FC = async () => { | |||
return "Projects"; | |||
const { t } = await getServerI18n("projects"); | |||
preloadProjects(); | |||
return ( | |||
<> | |||
<Stack | |||
direction="row" | |||
justifyContent="space-between" | |||
flexWrap="wrap" | |||
rowGap={2} | |||
> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("Projects")} | |||
</Typography> | |||
<Button | |||
variant="contained" | |||
startIcon={<Add />} | |||
LinkComponent={Link} | |||
href="/projects/create" | |||
> | |||
{t("Create Project")} | |||
</Button> | |||
</Stack> | |||
<Suspense fallback={<ProjectSearch.Loading />}> | |||
<ProjectSearch /> | |||
</Suspense> | |||
</> | |||
); | |||
}; | |||
export default Projects; |
@@ -0,0 +1,21 @@ | |||
import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
export const metadata: Metadata = { | |||
title: "Create Task Template", | |||
}; | |||
const Projects: React.FC = async () => { | |||
const { t } = await getServerI18n("tasks"); | |||
return ( | |||
<> | |||
<Typography variant="h4">{t("Create Task Template")}</Typography> | |||
<CreateTaskTemplate /> | |||
</> | |||
); | |||
}; | |||
export default Projects; |
@@ -1,11 +1,47 @@ | |||
import { preloadTaskTemplates } from "@/app/api/tasks"; | |||
import TaskTemplateSearch from "@/components/TaskTemplateSearch"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Add from "@mui/icons-material/Add"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
import Link from "next/link"; | |||
import { Suspense } from "react"; | |||
export const metadata: Metadata = { | |||
title: "Tasks", | |||
}; | |||
const Tasks: React.FC = async () => { | |||
return "Tasks"; | |||
const TaskTemplates: React.FC = async () => { | |||
const { t } = await getServerI18n("projects"); | |||
preloadTaskTemplates(); | |||
return ( | |||
<> | |||
<Stack | |||
direction="row" | |||
justifyContent="space-between" | |||
flexWrap="wrap" | |||
rowGap={2} | |||
> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("Task Template")} | |||
</Typography> | |||
<Button | |||
variant="contained" | |||
startIcon={<Add />} | |||
LinkComponent={Link} | |||
href="/tasks/create" | |||
> | |||
{t("Create Template")} | |||
</Button> | |||
</Stack> | |||
<Suspense fallback={<TaskTemplateSearch.Loading />}> | |||
<TaskTemplateSearch /> | |||
</Suspense> | |||
</> | |||
); | |||
}; | |||
export default Tasks; | |||
export default TaskTemplates; |
@@ -0,0 +1,46 @@ | |||
import { cache } from "react"; | |||
import "server-only"; | |||
export interface ProjectResult { | |||
id: number; | |||
code: string; | |||
name: string; | |||
category: "Confirmed Project" | "Project to be bidded"; | |||
team: string; | |||
client: string; | |||
} | |||
export const preloadProjects = () => { | |||
fetchProjects(); | |||
}; | |||
export const fetchProjects = cache(async () => { | |||
return mockProjects; | |||
}); | |||
const mockProjects: ProjectResult[] = [ | |||
{ | |||
id: 1, | |||
code: "M1001", | |||
name: "Consultancy Project A", | |||
category: "Confirmed Project", | |||
team: "TW", | |||
client: "Client A", | |||
}, | |||
{ | |||
id: 2, | |||
code: "M1002", | |||
name: "Consultancy Project B", | |||
category: "Project to be bidded", | |||
team: "WY", | |||
client: "Client B", | |||
}, | |||
{ | |||
id: 3, | |||
code: "S1001", | |||
name: "Consultancy Project C", | |||
category: "Confirmed Project", | |||
team: "WY", | |||
client: "Client C", | |||
}, | |||
]; |
@@ -0,0 +1,34 @@ | |||
import { cache } from "react"; | |||
import "server-only"; | |||
export interface TaskTemplateResult { | |||
id: number; | |||
code: string; | |||
name: string; | |||
} | |||
export const preloadTaskTemplates = () => { | |||
fetchTaskTemplates(); | |||
}; | |||
export const fetchTaskTemplates = cache(async () => { | |||
return mockProjects; | |||
}); | |||
const mockProjects: TaskTemplateResult[] = [ | |||
{ | |||
id: 1, | |||
code: "Pre-001", | |||
name: "Pre-contract Template", | |||
}, | |||
{ | |||
id: 2, | |||
code: "Post-001", | |||
name: "Post-contract Template", | |||
}, | |||
{ | |||
id: 3, | |||
code: "Full-001", | |||
name: "Full Project Template", | |||
}, | |||
]; |
@@ -14,7 +14,7 @@ export interface AppBarProps { | |||
const AppBar: React.FC<AppBarProps> = ({ avatarImageSrc, profileName }) => { | |||
return ( | |||
<I18nProvider namespaces={["common"]}> | |||
<MUIAppBar position="sticky"> | |||
<MUIAppBar position="sticky" color="default" elevation={4}> | |||
<Toolbar> | |||
<NavigationToggle /> | |||
<Box | |||
@@ -0,0 +1,51 @@ | |||
"use client"; | |||
import Breadcrumbs from "@mui/material/Breadcrumbs"; | |||
import Typography from "@mui/material/Typography"; | |||
import Link from "next/link"; | |||
import MUILink from "@mui/material/Link"; | |||
import { usePathname } from "next/navigation"; | |||
const pathToLabelMap: { [path: string]: string } = { | |||
"": "Overview", | |||
"/projects": "Projects", | |||
"/projects/create": "Create Project", | |||
"/tasks": "Task Template", | |||
"/tasks/create": "Create Task Template", | |||
}; | |||
const Breadcrumb = () => { | |||
const pathname = usePathname(); | |||
const segments = pathname.split("/"); | |||
return ( | |||
<Breadcrumbs> | |||
{segments.map((segment, index) => { | |||
const href = segments.slice(0, index + 1).join("/"); | |||
const label = pathToLabelMap[href] || segment; | |||
if (index === segments.length - 1) { | |||
return ( | |||
<Typography key={index} color="text.primary"> | |||
{label} | |||
</Typography> | |||
); | |||
} else { | |||
return ( | |||
<MUILink | |||
underline="hover" | |||
color="inherit" | |||
key={index} | |||
component={Link} | |||
href={href || "/"} | |||
> | |||
{label} | |||
</MUILink> | |||
); | |||
} | |||
})} | |||
</Breadcrumbs> | |||
); | |||
}; | |||
export default Breadcrumb; |
@@ -0,0 +1 @@ | |||
export { default } from "./Breadcrumb"; |
@@ -0,0 +1,53 @@ | |||
"use client"; | |||
import Check from "@mui/icons-material/Check"; | |||
import Close from "@mui/icons-material/Close"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Tab from "@mui/material/Tab"; | |||
import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||
import { useRouter } from "next/navigation"; | |||
import React, { useCallback, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import ProjectClientDetails from "./ProjectClientDetails"; | |||
import TaskSetup from "./TaskSetup"; | |||
const CreateProject: React.FC = () => { | |||
const [tabIndex, setTabIndex] = useState(0); | |||
const { t } = useTranslation(); | |||
const router = useRouter(); | |||
const handleCancel = () => { | |||
router.back(); | |||
}; | |||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
(_e, newValue) => { | |||
setTabIndex(newValue); | |||
}, | |||
[], | |||
); | |||
return ( | |||
<> | |||
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
<Tab label={t("Project and Client Details")} /> | |||
<Tab label={t("Project Task Setup")} /> | |||
<Tab label={t("Staff Allocation")} /> | |||
<Tab label={t("Resource and Milestone")} /> | |||
</Tabs> | |||
{tabIndex === 0 && <ProjectClientDetails />} | |||
{tabIndex === 1 && <TaskSetup />} | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | |||
{t("Cancel")} | |||
</Button> | |||
<Button variant="contained" startIcon={<Check />}> | |||
{t("Confirm")} | |||
</Button> | |||
</Stack> | |||
</> | |||
); | |||
}; | |||
export default CreateProject; |
@@ -0,0 +1,110 @@ | |||
"use client"; | |||
import Stack from "@mui/material/Stack"; | |||
import Box from "@mui/material/Box"; | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import FormControl from "@mui/material/FormControl"; | |||
import Grid from "@mui/material/Grid"; | |||
import InputLabel from "@mui/material/InputLabel"; | |||
import MenuItem from "@mui/material/MenuItem"; | |||
import Select from "@mui/material/Select"; | |||
import TextField from "@mui/material/TextField"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import Button from "@mui/material/Button"; | |||
const ProjectClientDetails: React.FC = () => { | |||
const { t } = useTranslation(); | |||
return ( | |||
<Card> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Project Details")} | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Code")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Subcode")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Name")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel>{t("Project Category")}</InputLabel> | |||
<Select | |||
label={t("Project Category")} | |||
value={"Temporary Project"} | |||
> | |||
<MenuItem value={"Temporary Project"}> | |||
{t("Temporary Project")} | |||
</MenuItem> | |||
</Select> | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel>{t("Team Lead")}</InputLabel> | |||
<Select label={t("Team Lead")} value={"00539 - Ming CHAN (MC)"}> | |||
<MenuItem value={"00539 - Ming CHAN (MC)"}> | |||
{"00539 - Ming CHAN (MC)"} | |||
</MenuItem> | |||
</Select> | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Description")} fullWidth /> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Client Details")} | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Code and Name")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Lead Name")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Lead Phone Number")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Lead Email")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel>{t("Client Subsidiary")}</InputLabel> | |||
<Select | |||
label={t("Client Subsidiary")} | |||
value={"Test Subsidiary"} | |||
> | |||
<MenuItem value={"Test Subsidiary"}> | |||
{t("Test Subsidiary")} | |||
</MenuItem> | |||
</Select> | |||
</FormControl> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default ProjectClientDetails; |
@@ -0,0 +1,70 @@ | |||
"use client"; | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import Grid from "@mui/material/Grid"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import TransferList from "../TransferList"; | |||
import Button from "@mui/material/Button"; | |||
import React from "react"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import FormControl from "@mui/material/FormControl"; | |||
import Select from "@mui/material/Select"; | |||
import MenuItem from "@mui/material/MenuItem"; | |||
import InputLabel from "@mui/material/InputLabel"; | |||
const TaskSetup = () => { | |||
const { t } = useTranslation(); | |||
return ( | |||
<Card> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Task List Setup")} | |||
</Typography> | |||
<Grid | |||
container | |||
spacing={2} | |||
columns={{ xs: 6, sm: 12 }} | |||
marginBlockEnd={1} | |||
> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel>{t("Task List Source")}</InputLabel> | |||
<Select | |||
label={t("Task List Source")} | |||
value={"M1009 - Consultancy Project X Temp"} | |||
> | |||
<MenuItem value={"M1009 - Consultancy Project X Temp"}> | |||
{"M1009 - Consultancy Project X Temp"} | |||
</MenuItem> | |||
</Select> | |||
</FormControl> | |||
</Grid> | |||
</Grid> | |||
<TransferList | |||
allItems={[ | |||
{ id: "1", label: "Task 1" }, | |||
{ id: "2", label: "Task 2" }, | |||
{ id: "3", label: "Task 3" }, | |||
{ id: "4", label: "Task 4" }, | |||
{ id: "5", label: "Task 5" }, | |||
]} | |||
initiallySelectedItems={[]} | |||
onChange={() => {}} | |||
allItemsLabel={t("Task Pool")} | |||
selectedItemsLabel={t("Project Task List")} | |||
/> | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default TaskSetup; |
@@ -0,0 +1 @@ | |||
export { default } from "./CreateProject"; |
@@ -0,0 +1,74 @@ | |||
"use client"; | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import Grid from "@mui/material/Grid"; | |||
import TextField from "@mui/material/TextField"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import TransferList from "../TransferList"; | |||
import Button from "@mui/material/Button"; | |||
import Check from "@mui/icons-material/Check"; | |||
import Close from "@mui/icons-material/Close"; | |||
import { useRouter } from "next/navigation"; | |||
import React from "react"; | |||
import Stack from "@mui/material/Stack"; | |||
const CreateTaskTemplate = () => { | |||
const { t } = useTranslation(); | |||
const router = useRouter(); | |||
const handleCancel = () => { | |||
router.back(); | |||
}; | |||
return ( | |||
<> | |||
<Card> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<Typography variant="overline">{t("Task List Setup")}</Typography> | |||
<Grid | |||
container | |||
spacing={2} | |||
columns={{ xs: 6, sm: 12 }} | |||
marginBlockEnd={1} | |||
> | |||
<Grid item xs={6}> | |||
<TextField label={t("Task Template Code")} fullWidth /> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Task Template Name")} fullWidth /> | |||
</Grid> | |||
</Grid> | |||
<TransferList | |||
allItems={[ | |||
{ id: "1", label: "Task 1: Super long task name that will overflow to the next line" }, | |||
{ id: "2", label: "Task 2" }, | |||
{ id: "3", label: "Task 3" }, | |||
{ id: "4", label: "Task 4" }, | |||
{ id: "5", label: "Task 5" }, | |||
{ id: "6", label: "Task 6" }, | |||
{ id: "7", label: "Task 7" }, | |||
{ id: "8", label: "Task 8" }, | |||
{ id: "9", label: "Task 9" }, | |||
]} | |||
initiallySelectedItems={[]} | |||
onChange={() => {}} | |||
allItemsLabel={t("Task Pool")} | |||
selectedItemsLabel={t("Task List Template")} | |||
/> | |||
</CardContent> | |||
</Card> | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | |||
{t("Cancel")} | |||
</Button> | |||
<Button variant="contained" startIcon={<Check />}> | |||
{t("Confirm")} | |||
</Button> | |||
</Stack> | |||
</> | |||
); | |||
}; | |||
export default CreateTaskTemplate; |
@@ -0,0 +1 @@ | |||
export { default } from "./CreateTaskTemplate"; |
@@ -0,0 +1,76 @@ | |||
"use client"; | |||
import { ProjectResult } from "@/app/api/projects"; | |||
import React, { useMemo, useState } from "react"; | |||
import SearchBox, { Criterion } from "../SearchBox"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
interface Props { | |||
projects: ProjectResult[]; | |||
} | |||
type SearchQuery = Partial<Omit<ProjectResult, "id">>; | |||
type SearchParamNames = keyof SearchQuery; | |||
const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||
const { t } = useTranslation("projects"); | |||
// If project searching is done on the server-side, then no need for this. | |||
const [filteredProjects, setFilteredProjects] = useState(projects); | |||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
() => [ | |||
{ label: t("Project code"), paramName: "code", type: "text" }, | |||
{ label: t("Project name"), paramName: "name", type: "text" }, | |||
{ | |||
label: t("Client name"), | |||
paramName: "client", | |||
type: "select", | |||
options: ["A", "B"], | |||
}, | |||
{ | |||
label: t("Project category"), | |||
paramName: "category", | |||
type: "select", | |||
options: ["A", "B"], | |||
}, | |||
{ | |||
label: t("Team"), | |||
paramName: "team", | |||
type: "select", | |||
options: ["A", "B"], | |||
}, | |||
], | |||
[t], | |||
); | |||
const columns = useMemo<Column<ProjectResult>[]>( | |||
() => [ | |||
{ name: "id", label: t("Details") }, | |||
{ name: "code", label: t("Project Code") }, | |||
{ name: "name", label: t("Project Name") }, | |||
{ name: "category", label: t("Project Category") }, | |||
{ name: "team", label: t("Team") }, | |||
{ name: "client", label: t("Client") }, | |||
], | |||
[t], | |||
); | |||
return ( | |||
<> | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
console.log(query); | |||
}} | |||
/> | |||
<SearchResults<ProjectResult> | |||
items={filteredProjects} | |||
columns={columns} | |||
/> | |||
</> | |||
); | |||
}; | |||
export default ProjectSearch; |
@@ -0,0 +1,40 @@ | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import Skeleton from "@mui/material/Skeleton"; | |||
import Stack from "@mui/material/Stack"; | |||
import React from "react"; | |||
// Can make this nicer | |||
export const ProjectSearchLoading: React.FC = () => { | |||
return ( | |||
<> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton | |||
variant="rounded" | |||
height={50} | |||
width={100} | |||
sx={{ alignSelf: "flex-end" }} | |||
/> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
</> | |||
); | |||
}; | |||
export default ProjectSearchLoading; |
@@ -0,0 +1,18 @@ | |||
import { fetchProjects } from "@/app/api/projects"; | |||
import React from "react"; | |||
import ProjectSearch from "./ProjectSearch"; | |||
import ProjectSearchLoading from "./ProjectSearchLoading"; | |||
interface SubComponents { | |||
Loading: typeof ProjectSearchLoading; | |||
} | |||
const ProjectSearchWrapper: React.FC & SubComponents = async () => { | |||
const projects = await fetchProjects(); | |||
return <ProjectSearch projects={projects} />; | |||
}; | |||
ProjectSearchWrapper.Loading = ProjectSearchLoading; | |||
export default ProjectSearchWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./ProjectSearchWrapper"; |
@@ -0,0 +1,135 @@ | |||
"use client"; | |||
import Grid from "@mui/material/Grid"; | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import Typography from "@mui/material/Typography"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import TextField from "@mui/material/TextField"; | |||
import FormControl from "@mui/material/FormControl"; | |||
import InputLabel from "@mui/material/InputLabel"; | |||
import Select, { SelectChangeEvent } from "@mui/material/Select"; | |||
import MenuItem from "@mui/material/MenuItem"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import Button from "@mui/material/Button"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import Search from "@mui/icons-material/Search"; | |||
interface BaseCriterion<T extends string> { | |||
label: string; | |||
paramName: T; | |||
} | |||
interface TextCriterion<T extends string> extends BaseCriterion<T> { | |||
type: "text"; | |||
} | |||
interface SelectCriterion<T extends string> extends BaseCriterion<T> { | |||
type: "select"; | |||
options: string[]; | |||
} | |||
export type Criterion<T extends string> = TextCriterion<T> | SelectCriterion<T>; | |||
interface Props<T extends string> { | |||
criteria: Criterion<T>[]; | |||
onSearch: (inputs: Record<T, string>) => void; | |||
} | |||
function SearchBox<T extends string>({ criteria, onSearch }: Props<T>) { | |||
const { t } = useTranslation("common"); | |||
const defaultInputs = useMemo( | |||
() => | |||
criteria.reduce<Record<T, string>>( | |||
(acc, c) => { | |||
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; | |||
}, | |||
{} as Record<T, string>, | |||
), | |||
[criteria], | |||
); | |||
const [inputs, setInputs] = useState(defaultInputs); | |||
const makeInputChangeHandler = useCallback( | |||
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => { | |||
return (e) => { | |||
setInputs((i) => ({ ...i, [paramName]: e.target.value })); | |||
}; | |||
}, | |||
[], | |||
); | |||
const makeSelectChangeHandler = useCallback((paramName: T) => { | |||
return (e: SelectChangeEvent) => { | |||
setInputs((i) => ({ ...i, [paramName]: e.target.value })); | |||
}; | |||
}, []); | |||
const handleReset = () => { | |||
setInputs(defaultInputs); | |||
}; | |||
const handleSearch = () => { | |||
onSearch(inputs); | |||
}; | |||
return ( | |||
<Card> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<Typography variant="overline">{t("Search Criteria")}</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
{criteria.map((c) => { | |||
return ( | |||
<Grid key={c.paramName} item xs={6}> | |||
{c.type === "text" && ( | |||
<TextField | |||
label={c.label} | |||
fullWidth | |||
onChange={makeInputChangeHandler(c.paramName)} | |||
value={inputs[c.paramName]} | |||
/> | |||
)} | |||
{c.type === "select" && ( | |||
<FormControl fullWidth> | |||
<InputLabel>{c.label}</InputLabel> | |||
<Select | |||
label={c.label} | |||
onChange={makeSelectChangeHandler(c.paramName)} | |||
value={inputs[c.paramName]} | |||
> | |||
<MenuItem value={"All"}>{t("All")}</MenuItem> | |||
{c.options.map((option) => ( | |||
<MenuItem key={option} value={option}> | |||
{option} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
</FormControl> | |||
)} | |||
</Grid> | |||
); | |||
})} | |||
</Grid> | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button | |||
variant="text" | |||
startIcon={<RestartAlt />} | |||
onClick={handleReset} | |||
> | |||
{t("Reset")} | |||
</Button> | |||
<Button | |||
variant="outlined" | |||
startIcon={<Search />} | |||
onClick={handleSearch} | |||
> | |||
{t("Search")} | |||
</Button> | |||
</CardActions> | |||
</CardContent> | |||
</Card> | |||
); | |||
} | |||
export default SearchBox; |
@@ -0,0 +1,2 @@ | |||
export { default } from "./SearchBox"; | |||
export type { Criterion } from "./SearchBox"; |
@@ -0,0 +1,100 @@ | |||
"use client"; | |||
import React from "react"; | |||
import Paper from "@mui/material/Paper"; | |||
import Table from "@mui/material/Table"; | |||
import TableBody from "@mui/material/TableBody"; | |||
import TableCell from "@mui/material/TableCell"; | |||
import TableContainer from "@mui/material/TableContainer"; | |||
import TableHead from "@mui/material/TableHead"; | |||
import TablePagination, { | |||
TablePaginationProps, | |||
} from "@mui/material/TablePagination"; | |||
import TableRow from "@mui/material/TableRow"; | |||
import IconButton from "@mui/material/IconButton"; | |||
import EditNote from "@mui/icons-material/EditNote"; | |||
interface ResultWithId { | |||
id: string | number; | |||
} | |||
export interface Column<T extends ResultWithId> { | |||
name: keyof T; | |||
label: string; | |||
} | |||
interface Props<T extends ResultWithId> { | |||
items: T[]; | |||
columns: Column<T>[]; | |||
} | |||
function SearchResults<T extends ResultWithId>({ items, columns }: Props<T>) { | |||
const [page, setPage] = React.useState(0); | |||
const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||
const handleChangePage: TablePaginationProps["onPageChange"] = ( | |||
_event, | |||
newPage, | |||
) => { | |||
setPage(newPage); | |||
}; | |||
const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( | |||
event, | |||
) => { | |||
setRowsPerPage(+event.target.value); | |||
setPage(0); | |||
}; | |||
return ( | |||
<Paper sx={{ overflow: "hidden" }}> | |||
<TableContainer sx={{ maxHeight: 440 }}> | |||
<Table stickyHeader> | |||
<TableHead> | |||
<TableRow> | |||
{columns.map((column) => ( | |||
<TableCell key={column.name.toString()}> | |||
{column.label} | |||
</TableCell> | |||
))} | |||
</TableRow> | |||
</TableHead> | |||
<TableBody> | |||
{items | |||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
.map((item) => { | |||
return ( | |||
<TableRow hover tabIndex={-1} key={item.id}> | |||
{columns.map(({ name: columnName }) => { | |||
return ( | |||
<TableCell key={columnName.toString()}> | |||
{columnName === "id" ? ( | |||
<IconButton color="primary"> | |||
<EditNote /> | |||
</IconButton> | |||
) : ( | |||
<>{item[columnName]}</> | |||
)} | |||
</TableCell> | |||
); | |||
})} | |||
</TableRow> | |||
); | |||
})} | |||
</TableBody> | |||
</Table> | |||
</TableContainer> | |||
<TablePagination | |||
rowsPerPageOptions={[10, 25, 100]} | |||
component="div" | |||
count={items.length} | |||
rowsPerPage={rowsPerPage} | |||
page={page} | |||
onPageChange={handleChangePage} | |||
onRowsPerPageChange={handleChangeRowsPerPage} | |||
/> | |||
</Paper> | |||
); | |||
} | |||
export default SearchResults; |
@@ -0,0 +1,2 @@ | |||
export { default } from "./SearchResults"; | |||
export type { Column } from "./SearchResults"; |
@@ -0,0 +1,52 @@ | |||
"use client"; | |||
import { TaskTemplateResult } from "@/app/api/tasks"; | |||
import React, { useMemo, useState } from "react"; | |||
import SearchBox, { Criterion } from "../SearchBox"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
interface Props { | |||
taskTemplates: TaskTemplateResult[]; | |||
} | |||
type SearchQuery = Partial<Omit<TaskTemplateResult, "id">>; | |||
type SearchParamNames = keyof SearchQuery; | |||
const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => { | |||
const { t } = useTranslation("tasks"); | |||
// If task searching is done on the server-side, then no need for this. | |||
const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates); | |||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
() => [ | |||
{ label: t("Task Template Code"), paramName: "code", type: "text" }, | |||
{ label: t("Task Template Name"), paramName: "name", type: "text" }, | |||
], | |||
[t], | |||
); | |||
const columns = useMemo<Column<TaskTemplateResult>[]>( | |||
() => [ | |||
{ name: "id", label: t("Details") }, | |||
{ name: "code", label: t("Task Template Code") }, | |||
{ name: "name", label: t("Task Template Name") }, | |||
], | |||
[t], | |||
); | |||
return ( | |||
<> | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
console.log(query); | |||
}} | |||
/> | |||
<SearchResults items={filteredTemplates} columns={columns} /> | |||
</> | |||
); | |||
}; | |||
export default TaskTemplateSearch; |
@@ -0,0 +1,38 @@ | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import Skeleton from "@mui/material/Skeleton"; | |||
import Stack from "@mui/material/Stack"; | |||
import React from "react"; | |||
// Can make this nicer | |||
export const TaskTemplateSearchLoading: React.FC = () => { | |||
return ( | |||
<> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton | |||
variant="rounded" | |||
height={50} | |||
width={100} | |||
sx={{ alignSelf: "flex-end" }} | |||
/> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
</> | |||
); | |||
}; | |||
export default TaskTemplateSearchLoading; |
@@ -0,0 +1,18 @@ | |||
import { fetchTaskTemplates } from "@/app/api/tasks"; | |||
import React from "react"; | |||
import TaskTemplateSearch from "./TaskTemplateSearch"; | |||
import TaskTemplateSearchLoading from "./TaskTemplateSearchLoading"; | |||
interface SubComponents { | |||
Loading: typeof TaskTemplateSearchLoading; | |||
} | |||
const TaskTemplateSearchWrapper: React.FC & SubComponents = async () => { | |||
const taskTemplates = await fetchTaskTemplates(); | |||
return <TaskTemplateSearch taskTemplates={taskTemplates} />; | |||
}; | |||
TaskTemplateSearchWrapper.Loading = TaskTemplateSearchLoading; | |||
export default TaskTemplateSearchWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./TaskTemplateSearchWrapper"; |
@@ -0,0 +1,143 @@ | |||
"use client"; | |||
import { | |||
Box, | |||
Checkbox, | |||
Divider, | |||
FormControl, | |||
InputLabel, | |||
ListItemIcon, | |||
ListItemText, | |||
ListSubheader, | |||
MenuItem, | |||
Select, | |||
SelectProps, | |||
Stack, | |||
Typography, | |||
} from "@mui/material"; | |||
import React, { useCallback, useState } from "react"; | |||
import { LabelWithId, TransferListProps } from "./TransferList"; | |||
export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
allItems, | |||
initiallySelectedItems, | |||
selectedItemsLabel, | |||
allItemsLabel, | |||
onChange, | |||
}) => { | |||
// Keep a map for the original order of items | |||
const sortMap = React.useMemo(() => { | |||
return allItems.reduce<{ [id: string]: LabelWithId & { index: number } }>( | |||
(acc, item, index) => ({ ...acc, [item.id]: { ...item, index } }), | |||
{}, | |||
); | |||
}, [allItems]); | |||
const compareFn = React.useCallback( | |||
(a: string, b: string) => sortMap[a].index - sortMap[b].index, | |||
[sortMap], | |||
); | |||
const [selectedItems, setSelectedItems] = useState( | |||
initiallySelectedItems.map((item) => item.id), | |||
); | |||
const handleChange = useCallback< | |||
NonNullable<SelectProps<typeof selectedItems>["onChange"]> | |||
>((event) => { | |||
const { | |||
target: { value }, | |||
} = event; | |||
setSelectedItems(typeof value === "string" ? [value] : value); | |||
}, []); | |||
const handleToggleAll = useCallback( | |||
() => () => { | |||
if (selectedItems.length === allItems.length) { | |||
setSelectedItems([]); | |||
} else { | |||
setSelectedItems(allItems.map((item) => item.id)); | |||
} | |||
}, | |||
[allItems, selectedItems.length], | |||
); | |||
return ( | |||
<Box> | |||
<FormControl fullWidth> | |||
<InputLabel>{selectedItemsLabel}</InputLabel> | |||
<Select | |||
multiple | |||
value={selectedItems} | |||
onChange={handleChange} | |||
renderValue={(values) => { | |||
return ( | |||
<Stack spacing={2}> | |||
{values.toSorted(compareFn).map((value) => ( | |||
<Typography key={value} whiteSpace="normal"> | |||
{sortMap[value].label} | |||
</Typography> | |||
))} | |||
</Stack> | |||
); | |||
}} | |||
MenuProps={{ | |||
slotProps: { | |||
paper: { | |||
sx: { maxHeight: 400 }, | |||
}, | |||
}, | |||
anchorOrigin: { | |||
vertical: "top", | |||
horizontal: "left", | |||
}, | |||
transformOrigin: { | |||
vertical: "top", | |||
horizontal: "left", | |||
}, | |||
}} | |||
> | |||
<ListSubheader disableGutters sx={{ zIndex: 1 }}> | |||
<Stack | |||
direction="row" | |||
paddingY={1} | |||
paddingX={3} | |||
onClick={handleToggleAll()} | |||
> | |||
<ListItemIcon> | |||
<Checkbox | |||
disableRipple | |||
checked={ | |||
selectedItems.length === allItems.length && | |||
allItems.length !== 0 | |||
} | |||
indeterminate={ | |||
selectedItems.length !== allItems.length && | |||
selectedItems.length !== 0 | |||
} | |||
/> | |||
</ListItemIcon> | |||
<Stack> | |||
<Typography variant="subtitle2">{allItemsLabel}</Typography> | |||
<Typography variant="caption">{`${selectedItems.length}/${allItems.length} selected`}</Typography> | |||
</Stack> | |||
</Stack> | |||
<Divider /> | |||
</ListSubheader> | |||
{allItems.map((item) => { | |||
return ( | |||
<MenuItem key={item.id} value={item.id} disableRipple> | |||
<Checkbox | |||
checked={selectedItems.includes(item.id)} | |||
disableRipple | |||
/> | |||
<ListItemText sx={{ whiteSpace: "normal" }}> | |||
{item.label} | |||
</ListItemText> | |||
</MenuItem> | |||
); | |||
})} | |||
</Select> | |||
</FormControl> | |||
</Box> | |||
); | |||
}; | |||
export default MultiSelectList; |
@@ -0,0 +1,214 @@ | |||
"use client"; | |||
import * as React from "react"; | |||
import List from "@mui/material/List"; | |||
import ListItem from "@mui/material/ListItem"; | |||
import ListItemText from "@mui/material/ListItemText"; | |||
import ListItemIcon from "@mui/material/ListItemIcon"; | |||
import Checkbox from "@mui/material/Checkbox"; | |||
import IconButton from "@mui/material/Fab"; | |||
import Divider from "@mui/material/Divider"; | |||
import ChevronLeft from "@mui/icons-material/ChevronLeft"; | |||
import ChevronRight from "@mui/icons-material/ChevronRight"; | |||
import intersection from "lodash/intersection"; | |||
import difference from "lodash/difference"; | |||
import Stack from "@mui/material/Stack"; | |||
import Paper from "@mui/material/Paper"; | |||
import Typography from "@mui/material/Typography"; | |||
import ListSubheader from "@mui/material/ListSubheader"; | |||
export interface LabelWithId { | |||
id: string; | |||
label: string; | |||
} | |||
export interface TransferListProps { | |||
allItems: LabelWithId[]; | |||
initiallySelectedItems: LabelWithId[]; | |||
onChange: () => void; | |||
allItemsLabel: string; | |||
selectedItemsLabel: string; | |||
} | |||
interface ItemListProps { | |||
items: LabelWithId[]; | |||
checkedItems: LabelWithId[]; | |||
label: string; | |||
handleToggleAll: ( | |||
items: LabelWithId[], | |||
checkedItems: LabelWithId[], | |||
) => React.MouseEventHandler; | |||
handleToggle: (item: LabelWithId) => React.MouseEventHandler; | |||
} | |||
const ItemList: React.FC<ItemListProps> = ({ | |||
items, | |||
checkedItems, | |||
label, | |||
handleToggle, | |||
handleToggleAll, | |||
}) => { | |||
return ( | |||
<Paper sx={{ width: "100%" }} variant="outlined"> | |||
<List | |||
sx={{ | |||
height: 400, | |||
bgcolor: "background.paper", | |||
overflow: "auto", | |||
}} | |||
disablePadding | |||
dense | |||
component="ul" | |||
subheader={ | |||
<ListSubheader | |||
disableGutters | |||
component="li" | |||
onClick={handleToggleAll(items, checkedItems)} | |||
> | |||
<Stack direction="row" paddingY={1} paddingX={2}> | |||
<ListItemIcon> | |||
<Checkbox | |||
checked={ | |||
checkedItems.length === items.length && items.length !== 0 | |||
} | |||
indeterminate={ | |||
checkedItems.length !== items.length && | |||
checkedItems.length !== 0 | |||
} | |||
disabled={items.length === 0} | |||
/> | |||
</ListItemIcon> | |||
<Stack> | |||
<Typography variant="subtitle2">{label}</Typography> | |||
<Typography variant="caption">{`${checkedItems.length}/${items.length} selected`}</Typography> | |||
</Stack> | |||
</Stack> | |||
<Divider /> | |||
</ListSubheader> | |||
} | |||
> | |||
{items.map((item) => { | |||
return ( | |||
<ListItem key={item.id} onClick={handleToggle(item)}> | |||
<ListItemIcon> | |||
<Checkbox checked={checkedItems.includes(item)} tabIndex={-1} /> | |||
</ListItemIcon> | |||
<ListItemText primary={item.label} /> | |||
</ListItem> | |||
); | |||
})} | |||
</List> | |||
</Paper> | |||
); | |||
}; | |||
const TransferList: React.FC<TransferListProps> = ({ | |||
allItems, | |||
initiallySelectedItems, | |||
allItemsLabel, | |||
selectedItemsLabel, | |||
onChange, | |||
}) => { | |||
// Keep a map for the original order of items | |||
const sortMap = React.useMemo(() => { | |||
return allItems.reduce<{ [id: string]: number }>( | |||
(acc, item, index) => ({ ...acc, [item.id]: index }), | |||
{}, | |||
); | |||
}, [allItems]); | |||
const compareFn = React.useCallback( | |||
(a: LabelWithId, b: LabelWithId) => sortMap[a.id] - sortMap[b.id], | |||
[sortMap], | |||
); | |||
const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]); | |||
const [leftList, setLeftList] = React.useState<LabelWithId[]>( | |||
difference(allItems, initiallySelectedItems), | |||
); | |||
const [rightList, setRightList] = React.useState<LabelWithId[]>( | |||
initiallySelectedItems, | |||
); | |||
const leftListChecked = intersection(checkedList, leftList); | |||
const rightListChecked = intersection(checkedList, rightList); | |||
const handleToggle = React.useCallback( | |||
(value: LabelWithId) => () => { | |||
const isChecked = checkedList.includes(value); | |||
const newCheckedList = isChecked | |||
? difference(checkedList, [value]) | |||
: [...checkedList, value]; | |||
setCheckedList(newCheckedList); | |||
}, | |||
[checkedList], | |||
); | |||
const handleToggleAll = React.useCallback( | |||
(items: LabelWithId[], checkedItems: LabelWithId[]) => () => { | |||
if (checkedItems.length === items.length) { | |||
setCheckedList(difference(checkedList, checkedItems)); | |||
} else { | |||
setCheckedList([...checkedList, ...items]); | |||
} | |||
}, | |||
[checkedList], | |||
); | |||
const handleCheckedRight = () => { | |||
setRightList([...rightList, ...leftListChecked].sort(compareFn)); | |||
setLeftList(difference(leftList, leftListChecked).sort(compareFn)); | |||
setCheckedList(difference(checkedList, leftListChecked)); | |||
}; | |||
const handleCheckedLeft = () => { | |||
setLeftList([...leftList, ...rightListChecked].sort(compareFn)); | |||
setRightList(difference(rightList, rightListChecked).sort(compareFn)); | |||
setCheckedList(difference(checkedList, rightListChecked)); | |||
}; | |||
return ( | |||
<Stack spacing={2} direction="row" alignItems="center" position="relative"> | |||
<ItemList | |||
items={leftList} | |||
checkedItems={leftListChecked} | |||
label={allItemsLabel} | |||
handleToggleAll={handleToggleAll} | |||
handleToggle={handleToggle} | |||
/> | |||
<ItemList | |||
items={rightList} | |||
checkedItems={rightListChecked} | |||
label={selectedItemsLabel} | |||
handleToggleAll={handleToggleAll} | |||
handleToggle={handleToggle} | |||
/> | |||
<Stack | |||
spacing={1} | |||
position="absolute" | |||
margin="0 !important" | |||
left="50%" | |||
sx={{ transform: "translateX(-50%)" }} | |||
> | |||
<IconButton | |||
color="secondary" | |||
size="small" | |||
onClick={handleCheckedRight} | |||
disabled={leftListChecked.length === 0} | |||
> | |||
<ChevronRight /> | |||
</IconButton> | |||
<IconButton | |||
color="secondary" | |||
size="small" | |||
onClick={handleCheckedLeft} | |||
disabled={rightListChecked.length === 0} | |||
> | |||
<ChevronLeft /> | |||
</IconButton> | |||
</Stack> | |||
</Stack> | |||
); | |||
}; | |||
export default TransferList; |
@@ -0,0 +1,15 @@ | |||
"use client"; | |||
import React from "react"; | |||
import TransferList, { TransferListProps } from "./TransferList"; | |||
import { useMediaQuery, useTheme } from "@mui/material"; | |||
import MultiSelectList from "./MultiSelectList"; | |||
const TransferListWrapper: React.FC<TransferListProps> = (props) => { | |||
const theme = useTheme(); | |||
const matches = useMediaQuery(theme.breakpoints.up("sm")); | |||
return matches ? <TransferList {...props} /> : <MultiSelectList {...props} />; | |||
}; | |||
export default TransferListWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./TransferListWrapper"; |
@@ -11,7 +11,7 @@ export const neutral = { | |||
900: "#111927", | |||
}; | |||
export const indigo = { | |||
export const primary = { | |||
lightest: "#F5F7FF", | |||
light: "#EBEEFE", | |||
main: "#6366F1", | |||
@@ -5,6 +5,13 @@ import palette from "./palette"; | |||
const muiTheme = createTheme(); | |||
const components: ThemeOptions["components"] = { | |||
MuiAppBar: { | |||
styleOverrides: { | |||
colorDefault: { | |||
backgroundColor: palette.background.paper, | |||
}, | |||
}, | |||
}, | |||
MuiAvatar: { | |||
styleOverrides: { | |||
root: { | |||
@@ -40,6 +47,26 @@ const components: ThemeOptions["components"] = { | |||
}, | |||
}, | |||
}, | |||
MuiPaper: { | |||
styleOverrides: { | |||
rounded: { | |||
borderRadius: 20, | |||
[`&.MuiPaper-elevation1`]: { | |||
boxShadow: | |||
"0px 5px 22px rgba(0, 0, 0, 0.04), 0px 0px 0px 0.5px rgba(0, 0, 0, 0.03)", | |||
}, | |||
}, | |||
outlined: { | |||
borderStyle: "solid", | |||
borderWidth: 1, | |||
overflow: "hidden", | |||
borderColor: palette.neutral[200], | |||
"&.MuiPaper-rounded": { | |||
borderRadius: 8, | |||
}, | |||
}, | |||
}, | |||
}, | |||
MuiCard: { | |||
styleOverrides: { | |||
root: { | |||
@@ -208,6 +235,7 @@ const components: ThemeOptions["components"] = { | |||
notchedOutline: { | |||
borderColor: palette.neutral[200], | |||
transition: muiTheme.transitions.create(["border-color", "box-shadow"]), | |||
legend: { width: 0 }, | |||
}, | |||
}, | |||
}, | |||
@@ -228,6 +256,8 @@ const components: ThemeOptions["components"] = { | |||
}, | |||
[`&.MuiInputLabel-outlined`]: { | |||
transform: "translate(14px, -9px) scale(0.85)", | |||
padding: "0 0.25rem", | |||
background: palette.primary.contrastText, | |||
}, | |||
}, | |||
}, | |||
@@ -267,7 +297,7 @@ const components: ThemeOptions["components"] = { | |||
color: palette.neutral[700], | |||
fontSize: 12, | |||
fontWeight: 600, | |||
lineHeight: 1, | |||
lineHeight: 2, | |||
letterSpacing: 0.5, | |||
textTransform: "uppercase", | |||
}, | |||
@@ -283,6 +313,13 @@ const components: ThemeOptions["components"] = { | |||
variant: "filled", | |||
}, | |||
}, | |||
MuiMenu: { | |||
styleOverrides: { | |||
list: { | |||
padding: 0, | |||
}, | |||
}, | |||
}, | |||
MuiMenuItem: { | |||
styleOverrides: { | |||
root: { | |||
@@ -299,6 +336,15 @@ const components: ThemeOptions["components"] = { | |||
}, | |||
}, | |||
}, | |||
MuiListItem: { | |||
styleOverrides: { | |||
root: { | |||
":hover": { | |||
backgroundColor: palette.neutral[100], | |||
}, | |||
}, | |||
}, | |||
}, | |||
MuiListItemButton: { | |||
styleOverrides: { | |||
root: { | |||
@@ -324,6 +370,13 @@ const components: ThemeOptions["components"] = { | |||
}, | |||
}, | |||
}, | |||
MuiSelect: { | |||
styleOverrides: { | |||
select: { | |||
borderRadius: 8, | |||
}, | |||
}, | |||
}, | |||
}; | |||
export default components; |
@@ -1,6 +1,6 @@ | |||
import { common } from "@mui/material/colors"; | |||
import { PaletteOptions } from "@mui/material/styles"; | |||
import { error, indigo, info, neutral, success, warning } from "./colors"; | |||
import { error, primary, info, neutral, success, warning } from "./colors"; | |||
const palette = { | |||
action: { | |||
@@ -19,7 +19,7 @@ const palette = { | |||
error, | |||
info, | |||
mode: "light", | |||
primary: indigo, | |||
primary, | |||
success, | |||
text: { | |||
primary: neutral[900], | |||