Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions scraper-frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
},
}
24 changes: 24 additions & 0 deletions scraper-frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
13 changes: 13 additions & 0 deletions scraper-frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scraper Frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
37 changes: 37 additions & 0 deletions scraper-frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "scraper-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.13.5",
"@mui/x-charts": "^7.17.0",
"@mui/x-data-grid": "^6.8.0",
"@types/dns-packet": "^5.2.4",
"@types/uuid": "^9.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.26.2",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react-swc": "^3.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^5.0.2",
"vite": "^4.3.9"
}
}
20 changes: 20 additions & 0 deletions scraper-frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Routes, Route, BrowserRouter } from "react-router-dom";
import { Home } from "./pages/Home";
import { ProductsPage } from "./pages/ProductsPage";
import { NoPage } from "./pages/NoPage";
import { ProductPage } from "./pages/ProductPage";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/product/:id" element={<ProductPage />} />
<Route path="*" element={<NoPage />} />
</Routes>
</BrowserRouter>
);
}

export default App;
55 changes: 55 additions & 0 deletions scraper-frontend/src/components/ProductList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { Box, Button } from "@mui/material";
import { DataGrid, GridCellParams, GridColDef } from "@mui/x-data-grid";
import { v4 as uuidv4 } from "uuid";
import { ProductInfo } from "../pages/ProductsPage";

export type ProductListProps = {
products: ProductInfo[];
};

export const ProductList = ({ products }: ProductListProps) => {
const navigate = useNavigate();

const columns = useMemo((): GridColDef[] => {
const renderCellWithButton = (params: GridCellParams) => {
const productId = params.row.id;

const onClick = (event: React.MouseEvent) => {
event.stopPropagation();
navigate(`/product/${productId}`);
};

return (
<Button variant="text" onClick={onClick} fullWidth>
View product
</Button>
);
};

return [
{ field: "name", headerName: "Name", flex: 1 },
{ field: "category", headerName: "Category", flex: 1 },
{ field: "domain", headerName: "Domain", flex: 1 },
{ field: "currency", headerName: "Currency", flex: 1 },
{
field: "action",
headerName: "",
width: 200,
sortable: false,
hideable: false,
filterable: false,
renderCell: renderCellWithButton,
},
];
}, [navigate]);

return (
<>
<Box alignContent={"center"} justifyContent={"center"} alignItems={"center"} padding={2} height={"800px"}>
<DataGrid rows={products} columns={columns} getRowId={() => uuidv4()} disableRowSelectionOnClick />
</Box>
</>
);
};
48 changes: 48 additions & 0 deletions scraper-frontend/src/components/ProductPriceChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMemo } from "react";
import { Box } from "@mui/material";
import { LineChart } from "@mui/x-charts";

export type ProductDataPoints = {
price: number;
date: string;
};

type ProductPriceChartProps = {
productName: string;
dataPoints: ProductDataPoints[];
currency: string;
};

export const ProductPriceChart = ({ productName, dataPoints, currency }: ProductPriceChartProps) => {
const productDates = useMemo(() => dataPoints.map((dataPoint) => new Date(dataPoint.date)), [dataPoints]);
const productPrices = useMemo(() => dataPoints.map((dataPoint) => dataPoint.price), [dataPoints]);

return (
<>
<Box height={600} width={1} boxShadow={3}>
<LineChart
xAxis={[
{
scaleType: "time",
data: productDates,
label: "Date",
},
]}
yAxis={[
{
label: `Price ${currency}`,
},
]}
series={[
{
label: productName,
data: productPrices,
valueFormatter: (value) => `${currency} ${value}`,
curve: "monotoneX",
},
]}
/>
</Box>
</>
);
};
9 changes: 9 additions & 0 deletions scraper-frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
15 changes: 15 additions & 0 deletions scraper-frontend/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Link } from "react-router-dom";
import { Button, Stack, Typography } from "@mui/material";

export const Home = () => {
return (
<>
<Stack alignItems={"center"} direction={"column"} spacing={6} paddingTop={10}>
<Typography fontSize={30}>Welcome</Typography>
<Link to={"/products"}>
<Button variant="contained">View your products</Button>
</Link>
</Stack>
</>
);
};
15 changes: 15 additions & 0 deletions scraper-frontend/src/pages/NoPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Link } from "react-router-dom";
import { Button, Stack, Typography } from "@mui/material";

export const NoPage = () => {
return (
<>
<Stack alignItems={"center"} justifyItems={"center"} paddingTop={8} spacing={4}>
<Typography fontSize={30}>404 - No page</Typography>
<Link to={"/"}>
<Button variant="contained">Go to homepage</Button>
</Link>
</Stack>
</>
);
};
76 changes: 76 additions & 0 deletions scraper-frontend/src/pages/ProductPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { Button, Divider, Stack, Typography } from "@mui/material";
import { ProductDataPoints, ProductPriceChart } from "../components/ProductPriceChart";
import { ProductInfo } from "./ProductsPage";
import { products, randomPrice } from "../test-data/products-data";

export const ProductPage = () => {
const { id } = useParams();

// TODO: Get product info from API
const productInfo = useMemo(() => {
return products.find((product) => product.id == id);
}, [id]);

// TODO: Get product data from API
const productDataPoints = useMemo((): ProductDataPoints[] => {
return [
{ date: "2024-09-15", price: randomPrice(400) },
{ date: "2024-09-16", price: randomPrice(1000) },
{ date: "2024-09-17", price: randomPrice(300) },
{ date: "2024-09-20", price: randomPrice(600) },
{ date: "2024-09-21", price: randomPrice(500) },
{ date: "2024-09-22", price: randomPrice(200) },
];
}, []);

const handleClickScrape = () => {
// TODO: call API
console.log(`Scrape product with id: ${id}`);
};

const handleClickDeactivate = () => {
// TODO: call API
console.log(`Deactivate product with id: ${id}`);
};

return (
<>
<Stack justifyContent={"center"} alignItems={"center"} direction={"column"}>
<ProductDetails productInfo={productInfo} />
<Stack alignItems={"center"} direction={"row"} justifyContent={"center"} spacing={2} padding={2}>
<Button variant="contained" onClick={handleClickScrape}>
Scrape product
</Button>
<Button variant="contained" onClick={handleClickDeactivate}>
{productInfo?.isActive ? "Deactivate" : "Activate"}
</Button>
</Stack>
<ProductPriceChart
productName={productInfo?.name ?? "N/A"}
dataPoints={productDataPoints}
currency={productInfo?.currency ?? "N/A"}
/>
</Stack>
</>
);
};

type ProductDetailsProps = {
productInfo?: ProductInfo;
};

const ProductDetails = ({ productInfo }: ProductDetailsProps) => {
return (
<>
<Typography fontSize={30} paddingTop={4}>
{productInfo?.name}
</Typography>
<Stack direction={"row"} spacing={2} divider={<Divider flexItem orientation="vertical" />}>
<Typography fontSize={20}>Category: {productInfo?.category}</Typography>
<Typography fontSize={20}>Domain: {productInfo?.domain}</Typography>
</Stack>
</>
);
};
27 changes: 27 additions & 0 deletions scraper-frontend/src/pages/ProductsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Stack, Typography } from "@mui/material";
import { ProductList } from "../components/ProductList";
import { products } from "../test-data/products-data";

export type ProductInfo = {
id: string;
name: string;
category: string;
domain: string;
currency: string;
isActive: boolean;
};

export const ProductsPage = () => {
// TODO: Get product data from API

return (
<>
<Stack direction={"column"} gap={2} paddingTop={2}>
<Typography fontSize={30} paddingLeft={2}>
Products
</Typography>
<ProductList products={products} />
</Stack>
</>
);
};
12 changes: 12 additions & 0 deletions scraper-frontend/src/test-data/products-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ProductInfo } from "../pages/ProductsPage";

// TODO: Get products from API
export const products: ProductInfo[] = [
{ id: "1", name: "Logitech Z533", category: "Speaker", domain: "Proshop", currency: "DKK", isActive: true },
{ id: "2", name: "Google Pixel 9", category: "Phone", domain: "Komplett", currency: "DKK", isActive: true },
{ id: "3", name: "Keycron K10", category: "Keyboard", domain: "ComputerSalg", currency: "DKK", isActive: false },
];

export const randomPrice = (upperLimit: number) => {
return Math.floor(Math.random() * upperLimit);
};
1 change: 1 addition & 0 deletions scraper-frontend/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
Loading