chore: wip ui

This commit is contained in:
Jayden Pyles
2025-06-08 11:34:42 -05:00
parent c1b3c68c76
commit 1f426989af
8 changed files with 521 additions and 292 deletions
@@ -1,7 +1,8 @@
import { Box, Link, Typography } from "@mui/material";
import { SetStateAction, Dispatch, useState } from "react";
import { AdvancedJobOptionsDialog } from "./dialog/advanced-job-options-dialog";
import { RawJobOptions } from "@/types";
import SettingsIcon from "@mui/icons-material/Settings";
import { Box, Button, Typography } from "@mui/material";
import { Dispatch, SetStateAction, useState } from "react";
import { AdvancedJobOptionsDialog } from "./dialog/advanced-job-options-dialog";
export type AdvancedJobOptionsProps = {
jobOptions: RawJobOptions;
@@ -17,26 +18,27 @@ export const AdvancedJobOptions = ({
const [open, setOpen] = useState(false);
return (
<Box sx={{ mb: 2 }}>
<Link
component="button"
variant="body2"
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button
variant="outlined"
onClick={() => setOpen(true)}
startIcon={<SettingsIcon />}
sx={{
textDecoration: "none",
color: "primary.main",
textTransform: "none",
borderRadius: 2,
px: 2,
py: 1,
borderColor: "divider",
color: "text.secondary",
"&:hover": {
color: "primary.dark",
textDecoration: "underline",
borderColor: "primary.main",
color: "primary.main",
bgcolor: "action.hover",
},
paddingLeft: 1,
display: "inline-flex",
alignItems: "center",
gap: 0.5,
}}
>
<Typography variant="body2">Advanced Job Options</Typography>
</Link>
<Typography variant="body2">Advanced Options</Typography>
</Button>
<AdvancedJobOptionsDialog
open={open}
+10 -11
View File
@@ -1,14 +1,14 @@
"use client";
import React, { useEffect, useRef } from "react";
import { Container, Box } from "@mui/material";
import { useRouter } from "next/router";
import { ElementTable, JobSubmitter } from "@/components/submit/job-submitter";
import { useJobSubmitterProvider } from "@/components/submit/job-submitter/provider";
import {
ErrorSnackbar,
JobNotifySnackbar,
} from "@/components/common/snackbars";
import { ElementTable, JobSubmitter } from "@/components/submit/job-submitter";
import { useJobSubmitterProvider } from "@/components/submit/job-submitter/provider";
import { Box, Container } from "@mui/material";
import { useRouter } from "next/router";
import { useEffect, useRef } from "react";
export const Home = () => {
const {
@@ -50,19 +50,18 @@ export const Home = () => {
flexDirection="column"
justifyContent="center"
alignItems="center"
height="100%"
minHeight="100vh"
py={4}
>
<Container maxWidth="lg" className="overflow-y-auto max-h-full">
<JobSubmitter />
{submittedURL.length > 0 ? (
<Container maxWidth="lg" className="overflow-y-auto">
<Box className="flex flex-col gap-6">
<JobSubmitter />
<ElementTable
rows={rows}
setRows={setRows}
submittedURL={submittedURL}
/>
) : null}
</Box>
</Container>
{snackbarSeverity === "info" ? (
@@ -2,11 +2,12 @@
import { Element } from "@/types";
import AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/Delete";
import {
Box,
Button,
Divider,
IconButton,
Paper,
Table,
TableBody,
TableCell,
@@ -40,196 +41,224 @@ export const ElementTable = ({ rows, setRows, submittedURL }: Props) => {
};
const handleDeleteRow = (elementName: string) => {
setRows(
rows.filter((r) => {
return elementName !== r.name;
})
);
setRows(rows.filter((r) => elementName !== r.name));
};
return (
<Box
className="animate-fadeIn"
bgcolor="background.paper"
<Paper
elevation={0}
sx={{
p: 4,
borderRadius: 2,
boxShadow: 1,
p: 3,
mb: 4,
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
transition: "all 0.3s ease-in-out",
"&:hover": {
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.05)",
},
}}
>
<Typography
variant="h5"
sx={{
mb: 3,
fontWeight: 600,
color: "text.primary",
}}
>
Elements to Scrape
</Typography>
<TableContainer
component={Box}
sx={{
maxHeight: "400px",
overflow: "auto",
borderRadius: 1,
border: 1,
borderColor: "divider",
}}
>
<Table
stickyHeader
size="small"
sx={{
tableLayout: "fixed",
width: "100%",
"& .MuiTableCell-root": {
borderBottom: "1px solid",
borderColor: "divider",
py: 1.5,
},
"& .MuiTableCell-head": {
bgcolor: "background.default",
<Box className="flex flex-col gap-6">
<Box>
<Typography
variant="h5"
sx={{
fontWeight: 600,
},
color: "text.primary",
mb: 1,
}}
>
Elements to Scrape
</Typography>
<Typography
variant="body2"
sx={{
color: "text.secondary",
}}
>
Add elements to scrape from the target URL using XPath selectors
</Typography>
</Box>
<TableContainer
component={Box}
sx={{
maxHeight: "400px",
overflow: "auto",
borderRadius: 2,
border: 1,
borderColor: "divider",
}}
>
<TableHead>
<TableRow>
<TableCell width="30%">Name</TableCell>
<TableCell width="50%">XPath</TableCell>
<TableCell width="20%" align="center">
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>
<TextField
data-cy="name-field"
placeholder="Enter element name"
variant="outlined"
fullWidth
size="small"
value={newRow.name}
onChange={(e) =>
setNewRow({ ...newRow, name: e.target.value })
}
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "background.default",
},
}}
/>
</TableCell>
<TableCell>
<TextField
data-cy="xpath-field"
placeholder="Enter XPath selector"
variant="outlined"
fullWidth
size="small"
value={newRow.xpath}
onChange={(e) =>
setNewRow({ ...newRow, xpath: e.target.value })
}
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "background.default",
},
}}
/>
</TableCell>
<TableCell align="center">
<Tooltip
title={
newRow.xpath.length > 0 && newRow.name.length > 0
? "Add Element"
: "Fill out all fields to add an element"
}
placement="top"
>
<span>
<IconButton
data-cy="add-button"
aria-label="add"
size="small"
onClick={handleAddRow}
disabled={
!(newRow.xpath.length > 0 && newRow.name.length > 0)
}
sx={{
bgcolor: "primary.main",
color: "primary.contrastText",
<Table
stickyHeader
size="small"
sx={{
"& .MuiTableCell-root": {
borderBottom: "1px solid",
borderColor: "divider",
py: 1.5,
},
"& .MuiTableCell-head": {
bgcolor: "background.default",
fontWeight: 600,
},
}}
>
<TableHead>
<TableRow>
<TableCell width="30%">Name</TableCell>
<TableCell width="50%">XPath</TableCell>
<TableCell width="20%" align="center">
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>
<TextField
data-cy="name-field"
placeholder="Enter element name"
variant="outlined"
fullWidth
size="small"
value={newRow.name}
onChange={(e) =>
setNewRow({ ...newRow, name: e.target.value })
}
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: 2,
bgcolor: "background.default",
transition: "all 0.2s ease-in-out",
"&:hover": {
bgcolor: "primary.dark",
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "primary.main",
},
},
"&.Mui-disabled": {
bgcolor: "action.disabledBackground",
color: "action.disabled",
},
}}
/>
</TableCell>
<TableCell>
<TextField
data-cy="xpath-field"
placeholder="Enter XPath selector"
variant="outlined"
fullWidth
size="small"
value={newRow.xpath}
onChange={(e) =>
setNewRow({ ...newRow, xpath: e.target.value })
}
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: 2,
bgcolor: "background.default",
transition: "all 0.2s ease-in-out",
"&:hover": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "primary.main",
},
},
},
}}
/>
</TableCell>
<TableCell align="center">
<Tooltip
title={
newRow.xpath.length > 0 && newRow.name.length > 0
? "Add Element"
: "Fill out all fields to add an element"
}
placement="top"
>
<span>
<IconButton
data-cy="add-button"
aria-label="add"
size="small"
onClick={handleAddRow}
disabled={
!(newRow.xpath.length > 0 && newRow.name.length > 0)
}
sx={{
bgcolor: "primary.main",
color: "primary.contrastText",
borderRadius: 2,
transition: "all 0.2s ease-in-out",
"&:hover": {
bgcolor: "primary.dark",
transform: "translateY(-1px)",
},
"&.Mui-disabled": {
bgcolor: "action.disabledBackground",
color: "action.disabled",
},
}}
>
<AddIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</TableCell>
</TableRow>
{rows.map((row, index) => (
<TableRow
key={index}
sx={{
"&:hover": {
bgcolor: "action.hover",
},
}}
>
<TableCell>
<Typography variant="body2" noWrap>
{row.name}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: "0.875rem",
color: "text.secondary",
}}
noWrap
>
{row.xpath}
</Typography>
</TableCell>
<TableCell align="center">
<IconButton
onClick={() => handleDeleteRow(row.name)}
size="small"
color="error"
sx={{
transition: "all 0.2s ease-in-out",
"&:hover": {
bgcolor: "error.main",
color: "error.contrastText",
transform: "translateY(-1px)",
},
}}
>
<AddIcon fontSize="small" />
<DeleteIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</TableCell>
</TableRow>
{rows.map((row, index) => (
<TableRow
key={index}
sx={{
"&:hover": {
bgcolor: "action.hover",
},
}}
>
<TableCell>
<Typography variant="body2" noWrap>
{row.name}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: "0.875rem",
color: "text.secondary",
}}
noWrap
>
{row.xpath}
</Typography>
</TableCell>
<TableCell align="center">
<Button
onClick={() => handleDeleteRow(row.name)}
size="small"
variant="outlined"
color="error"
sx={{
minWidth: "80px",
textTransform: "none",
"&:hover": {
bgcolor: "error.main",
color: "error.contrastText",
},
}}
>
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Divider sx={{ my: 3 }} />
<SiteMap />
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Divider sx={{ my: 2 }} />
<SiteMap />
</Box>
</Paper>
);
};
@@ -1,6 +1,5 @@
import { Box, Typography } from "@mui/material";
import React, { ReactNode } from "react";
import { Typography } from "@mui/material";
import classes from "./job-submitter-header.module.css";
interface JobSubmitterHeaderProps {
title?: string;
@@ -8,13 +7,26 @@ interface JobSubmitterHeaderProps {
}
export const JobSubmitterHeader: React.FC<JobSubmitterHeaderProps> = ({
title = "Scraping Made Easy",
title = "Scrape Webpage",
children,
}) => {
return (
<div className={classes.jobSubmitterHeader}>
<Typography variant="h3">{title}</Typography>
{children}
</div>
<Box
sx={{
textAlign: "left",
mb: 1,
}}
>
<Typography
variant="h4"
sx={{
fontWeight: 600,
color: "text.primary",
mb: 1,
}}
>
{title}
</Typography>
</Box>
);
};
@@ -1,5 +1,4 @@
import React from "react";
import { TextField, Button, CircularProgress } from "@mui/material";
import { Box, Button, CircularProgress, TextField } from "@mui/material";
import { useJobSubmitterProvider } from "../provider";
export type JobSubmitterInputProps = {
@@ -17,7 +16,14 @@ export const JobSubmitterInput = ({
useJobSubmitterProvider();
return (
<div className="flex flex-row space-x-4 items-center mb-2">
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", sm: "row" },
gap: 2,
alignItems: { xs: "stretch", sm: "center" },
}}
>
<TextField
data-cy="url-input"
label="URL"
@@ -27,19 +33,44 @@ export const JobSubmitterInput = ({
onChange={(e) => setSubmittedURL(e.target.value)}
error={!isValidURL}
helperText={!isValidURL ? urlError : ""}
className="rounded-md"
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: 2,
transition: "all 0.2s ease-in-out",
"&:hover": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "primary.main",
},
},
},
}}
/>
<Button
data-cy="submit-button"
variant="contained"
size="small"
size="large"
onClick={handleSubmit}
disabled={!(rows.length > 0) || loading}
className={`bg-[#034efc] text-white font-semibold rounded-md
transition-transform transform hover:scale-105 disabled:opacity-50`}
sx={{
minWidth: { xs: "100%", sm: 120 },
height: { xs: 48, sm: 56 },
borderRadius: 2,
textTransform: "none",
fontSize: "1rem",
fontWeight: 500,
transition: "all 0.2s ease-in-out",
"&:hover": {
transform: "translateY(-1px)",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
},
"&:disabled": {
transform: "none",
boxShadow: "none",
},
}}
>
{loading ? <CircularProgress size={24} color="inherit" /> : "Submit"}
</Button>
</div>
</Box>
);
};
@@ -4,6 +4,7 @@ import { AdvancedJobOptions } from "@/components/common/advanced-job-options";
import { useSubmitJob } from "@/hooks/use-submit-job";
import { parseJobOptions } from "@/lib";
import { useUser } from "@/store/hooks";
import { Box, Fade, Paper } from "@mui/material";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { JobSubmitterHeader } from "./job-submitter-header";
@@ -35,17 +36,36 @@ export const JobSubmitter = () => {
}, [jobOptions]);
return (
<div>
<JobSubmitterHeader />
<JobSubmitterInput
urlError={error}
handleSubmit={handleSubmit}
loading={loading}
/>
<AdvancedJobOptions
jobOptions={jobOptions}
setJobOptions={setJobOptions}
/>
</div>
<Fade in timeout={500}>
<Paper
elevation={0}
sx={{
p: 4,
borderRadius: 2,
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
transition: "all 0.3s ease-in-out",
"&:hover": {
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.05)",
},
}}
>
<Box className="flex flex-col gap-6">
<JobSubmitterHeader />
<Box className="flex flex-col gap-4">
<JobSubmitterInput
urlError={error}
handleSubmit={handleSubmit}
loading={loading}
/>
<AdvancedJobOptions
jobOptions={jobOptions}
setJobOptions={setJobOptions}
/>
</Box>
</Box>
</Paper>
</Fade>
);
};
@@ -1,17 +1,17 @@
import { useState } from "react";
import { useJobSubmitterProvider } from "../../provider";
import { ActionOption } from "@/types/job";
import {
Box,
Button,
Checkbox,
FormControl,
FormControlLabel,
InputLabel,
MenuItem,
Select,
TextField,
FormControl,
Button,
Checkbox,
FormControlLabel,
} from "@mui/material";
import { ActionOption } from "@/types/job";
import classes from "./site-map-input.module.css";
import { clsx } from "clsx";
import { useState } from "react";
import { useJobSubmitterProvider } from "../../provider";
export type SiteMapInputProps = {
disabled?: boolean;
@@ -28,7 +28,6 @@ export const SiteMapInput = ({
clickOnce,
input,
}: SiteMapInputProps) => {
console.log(clickOnce);
const [optionState, setOptionState] = useState<ActionOption>(
option || "click"
);
@@ -43,8 +42,6 @@ export const SiteMapInput = ({
const handleAdd = () => {
if (!siteMap) return;
console.log(optionState, xpathState, clickOnceState, inputState);
setSiteMap((prevSiteMap) => ({
...prevSiteMap,
actions: [
@@ -60,6 +57,7 @@ export const SiteMapInput = ({
}));
setXpathState("");
setInputState("");
};
const handleRemove = () => {
@@ -72,14 +70,22 @@ export const SiteMapInput = ({
};
return (
<div className="flex flex-col gap-2 w-full">
<div className="flex gap-2 items-center">
<FormControl className="w-1/4">
<Box
sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%" }}
>
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
<FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel>Action Type</InputLabel>
<Select
disabled={disabled}
displayEmpty
value={optionState}
label="Action Type"
onChange={(e) => setOptionState(e.target.value as ActionOption)}
sx={{
"& .MuiSelect-select": {
textTransform: "capitalize",
},
}}
>
<MenuItem value="click">Click</MenuItem>
<MenuItem value="input">Input</MenuItem>
@@ -88,23 +94,49 @@ export const SiteMapInput = ({
{optionState === "input" && (
<TextField
label="Input Text"
size="small"
fullWidth
value={inputState}
onChange={(e) => setInputState(e.target.value)}
disabled={disabled}
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "background.default",
},
}}
/>
)}
{!disabled && (
<TextField
label="XPath Selector"
size="small"
fullWidth
value={xpathState}
onChange={(e) => setXpathState(e.target.value)}
disabled={disabled}
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "background.default",
fontFamily: "monospace",
fontSize: "1rem",
},
}}
/>
)}
<TextField
label="XPath Selector"
fullWidth
value={xpathState}
onChange={(e) => setXpathState(e.target.value)}
disabled={disabled}
/>
{disabled ? (
<Button
onClick={handleRemove}
className={clsx(classes.button, classes.remove)}
size="small"
variant="outlined"
color="error"
sx={{
minWidth: "80px",
textTransform: "none",
"&:hover": {
bgcolor: "error.main",
color: "error.contrastText",
},
}}
>
Delete
</Button>
@@ -112,24 +144,41 @@ export const SiteMapInput = ({
<Button
onClick={handleAdd}
disabled={!xpathState}
className={clsx(classes.button, classes.add)}
size="small"
variant="contained"
color="primary"
sx={{
minWidth: "80px",
textTransform: "none",
"&.Mui-disabled": {
bgcolor: "action.disabledBackground",
color: "action.disabled",
},
}}
>
Add
</Button>
)}
</div>
</Box>
{!disabled && (
<FormControlLabel
label="Do Once"
control={
<Checkbox
size="small"
checked={clickOnceState}
disabled={disabled}
onChange={() => setClickOnceState(!clickOnceState)}
/>
}
sx={{
"& .MuiFormControlLabel-label": {
fontSize: "0.875rem",
color: "text.secondary",
},
}}
/>
)}
</div>
</Box>
);
};
@@ -1,12 +1,22 @@
import {
Box,
Button,
Divider,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { useEffect, useState } from "react";
import { useJobSubmitterProvider } from "../provider";
import { Button, Divider, Typography, useTheme } from "@mui/material";
import { SiteMapInput } from "./site-map-input";
export const SiteMap = () => {
const { siteMap, setSiteMap } = useJobSubmitterProvider();
const [showSiteMap, setShowSiteMap] = useState<boolean>(false);
const theme = useTheme();
const handleCreateSiteMap = () => {
setSiteMap({ actions: [] });
@@ -25,46 +35,123 @@ export const SiteMap = () => {
}, [siteMap]);
return (
<div className="flex flex-col gap-4">
{siteMap ? (
<Button onClick={handleClearSiteMap}>Clear Site Map</Button>
<Box className="flex flex-col gap-4">
{!siteMap ? (
<Button
onClick={handleCreateSiteMap}
variant="contained"
color="primary"
sx={{
alignSelf: "flex-end",
textTransform: "none",
}}
>
Create Site Map
</Button>
) : (
<Button onClick={handleCreateSiteMap}>Create Site Map</Button>
)}
{showSiteMap && (
<div className="flex flex-col gap-4">
<Box className="flex flex-col gap-4">
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography variant="h6" sx={{ fontWeight: 500 }}>
Site Map Configuration
</Typography>
<Button
onClick={handleClearSiteMap}
variant="outlined"
color="error"
size="small"
sx={{
textTransform: "none",
"&:hover": {
bgcolor: "error.main",
color: "error.contrastText",
},
}}
>
Clear Site Map
</Button>
</Box>
<SiteMapInput />
{siteMap?.actions && siteMap?.actions.length > 0 && (
<>
<Divider
<Divider />
<TableContainer
sx={{
borderColor:
theme.palette.mode === "dark" ? "#ffffff" : "0000000",
maxHeight: "400px",
overflow: "auto",
borderRadius: 1,
border: 1,
borderColor: "divider",
}}
/>
<Typography className="w-full text-center" variant="h5">
Site Map Actions
</Typography>
>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell width="10%">
<Typography sx={{ fontWeight: 600 }}>Action</Typography>
</TableCell>
<TableCell width="30%">
<Typography sx={{ fontWeight: 600 }}>Type</Typography>
</TableCell>
<TableCell width="40%">
<Typography sx={{ fontWeight: 600 }}>XPath</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{siteMap?.actions.reverse().map((action, index) => (
<TableRow
key={action.xpath}
sx={{
"&:hover": {
bgcolor: "action.hover",
},
}}
>
<TableCell>
<Typography variant="body2">{index + 1}</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
sx={{
color:
action.type === "click"
? "primary.main"
: "warning.main",
fontWeight: 500,
}}
>
{action.type}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: "0.875rem",
color: "text.secondary",
}}
noWrap
>
{action.xpath}
</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
)}
<ul className="flex flex-col gap-4">
{siteMap?.actions.reverse().map((action, index) => (
<li key={action.xpath} className="flex w-full items-center">
<Typography variant="h6" className="w-[10%] mr-2">
Action {index + 1}:
</Typography>
<SiteMapInput
disabled={Boolean(siteMap)}
xpath={action.xpath}
option={action.type}
clickOnce={action.do_once}
input={action.input}
/>
</li>
))}
</ul>
</div>
</Box>
)}
</div>
</Box>
);
};