implemented service page design

This commit is contained in:
strNophix 2022-05-29 11:20:23 +02:00
parent 4fd02288cd
commit 72123aee2f
13 changed files with 409 additions and 14 deletions

View File

@ -9,13 +9,16 @@
},
"dependencies": {
"@babel/core": ">=7.0.0 <8.0.0",
"@headlessui/react": "^1.6.3",
"@heroicons/react": "^1.0.6",
"clsx": "^1.1.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-query": "^3.39.0",
"react-router-dom": "6"
},
"devDependencies": {
"@faker-js/faker": "^7.1.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^1.3.0",

168
frontend/pnpm-lock.yaml generated
View File

@ -2,6 +2,8 @@ lockfileVersion: 5.4
specifiers:
'@babel/core': '>=7.0.0 <8.0.0'
'@faker-js/faker': ^7.1.0
'@headlessui/react': ^1.6.3
'@heroicons/react': ^1.0.6
'@types/react': ^18.0.0
'@types/react-dom': ^18.0.0
@ -11,6 +13,7 @@ specifiers:
postcss: ^8.4.14
react: ^18.0.0
react-dom: ^18.0.0
react-query: ^3.39.0
react-router-dom: '6'
tailwindcss: ^3.0.24
typescript: ^4.6.3
@ -18,13 +21,16 @@ specifiers:
dependencies:
'@babel/core': 7.17.10
'@headlessui/react': 1.6.3_ef5jwxihqo6n7gxfmzogljlgcm
'@heroicons/react': 1.0.6_react@18.1.0
clsx: 1.1.1
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
react-query: 3.39.0_ef5jwxihqo6n7gxfmzogljlgcm
react-router-dom: 6.3.0_ef5jwxihqo6n7gxfmzogljlgcm
devDependencies:
'@faker-js/faker': 7.1.0
'@types/react': 18.0.9
'@types/react-dom': 18.0.4
'@vitejs/plugin-react': 1.3.2
@ -283,6 +289,22 @@ packages:
'@babel/helper-validator-identifier': 7.16.7
to-fast-properties: 2.0.0
/@faker-js/faker/7.1.0:
resolution: {integrity: sha512-G+EvE29QUd9/6GTrwA/TK2AiN79W4ZG6kyJqj2RAqUqEd8A8ICnjA3Rj3F2IaaA+sq6WJzs4lh8GJJRDG1UH7A==}
engines: {node: '>=14.0.0', npm: '>=6.0.0'}
dev: true
/@headlessui/react/1.6.3_ef5jwxihqo6n7gxfmzogljlgcm:
resolution: {integrity: sha512-WNu/ypGzl0JmJ+sD34KtdycEu2n7EZjKFx2rq6fivsszPdoEyOVZ/GYQMJ437dfAJI0/ZxoRYfrOVduZHjlokQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
dependencies:
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
dev: false
/@heroicons/react/1.0.6_react@18.1.0:
resolution: {integrity: sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==}
peerDependencies:
@ -435,11 +457,27 @@ packages:
postcss-value-parser: 4.2.0
dev: true
/balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: false
/big-integer/1.6.51:
resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
engines: {node: '>=0.6'}
dev: false
/binary-extensions/2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
dev: true
/brace-expansion/1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
dev: false
/braces/3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
@ -447,6 +485,19 @@ packages:
fill-range: 7.0.1
dev: true
/broadcast-channel/3.7.0:
resolution: {integrity: sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==}
dependencies:
'@babel/runtime': 7.18.0
detect-node: 2.1.0
js-sha3: 0.8.0
microseconds: 0.2.0
nano-time: 1.0.0
oblivious-set: 1.0.0
rimraf: 3.0.2
unload: 2.2.0
dev: false
/browserslist/4.20.3:
resolution: {integrity: sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@ -506,6 +557,10 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
dev: false
/convert-source-map/1.8.0:
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
dependencies:
@ -536,6 +591,10 @@ packages:
resolution: {integrity: sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=}
dev: true
/detect-node/2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
dev: false
/detective/5.2.0:
resolution: {integrity: sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==}
engines: {node: '>=0.8.0'}
@ -805,6 +864,10 @@ packages:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: true
/fs.realpath/1.0.0:
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
dev: false
/fsevents/2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -835,6 +898,17 @@ packages:
is-glob: 4.0.3
dev: true
/glob/7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
dev: false
/globals/11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
@ -856,6 +930,17 @@ packages:
'@babel/runtime': 7.18.0
dev: false
/inflight/1.0.6:
resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=}
dependencies:
once: 1.4.0
wrappy: 1.0.2
dev: false
/inherits/2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/is-binary-path/2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@ -886,6 +971,10 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
/js-sha3/0.8.0:
resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==}
dev: false
/js-tokens/4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -911,6 +1000,13 @@ packages:
js-tokens: 4.0.0
dev: false
/match-sorter/6.3.1:
resolution: {integrity: sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==}
dependencies:
'@babel/runtime': 7.18.0
remove-accents: 0.4.2
dev: false
/merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@ -924,6 +1020,16 @@ packages:
picomatch: 2.3.1
dev: true
/microseconds/0.2.0:
resolution: {integrity: sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==}
dev: false
/minimatch/3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.11
dev: false
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: true
@ -931,6 +1037,12 @@ packages:
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
/nano-time/1.0.0:
resolution: {integrity: sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=}
dependencies:
big-integer: 1.6.51
dev: false
/nanoid/3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -955,6 +1067,21 @@ packages:
engines: {node: '>= 6'}
dev: true
/oblivious-set/1.0.0:
resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==}
dev: false
/once/1.4.0:
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
dependencies:
wrappy: 1.0.2
dev: false
/path-is-absolute/1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
engines: {node: '>=0.10.0'}
dev: false
/path-parse/1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
@ -1044,6 +1171,25 @@ packages:
scheduler: 0.22.0
dev: false
/react-query/3.39.0_ef5jwxihqo6n7gxfmzogljlgcm:
resolution: {integrity: sha512-Od0IkSuS79WJOhzWBx/ys0x13+7wFqgnn64vBqqAAnZ9whocVhl/y1padD5uuZ6EIkXbFbInax0qvY7zGM0thA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.18.0
broadcast-channel: 3.7.0
match-sorter: 6.3.1
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
dev: false
/react-refresh/0.13.0:
resolution: {integrity: sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==}
engines: {node: '>=0.10.0'}
@ -1088,6 +1234,10 @@ packages:
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
dev: false
/remove-accents/0.4.2:
resolution: {integrity: sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=}
dev: false
/resolve/1.22.0:
resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
hasBin: true
@ -1102,6 +1252,13 @@ packages:
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
dev: true
/rimraf/3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
hasBin: true
dependencies:
glob: 7.2.3
dev: false
/rollup/2.72.1:
resolution: {integrity: sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA==}
engines: {node: '>=10.0.0'}
@ -1192,6 +1349,13 @@ packages:
hasBin: true
dev: true
/unload/2.2.0:
resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==}
dependencies:
'@babel/runtime': 7.18.0
detect-node: 2.1.0
dev: false
/util-deprecate/1.0.2:
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
dev: true
@ -1220,6 +1384,10 @@ packages:
fsevents: 2.3.2
dev: true
/wrappy/1.0.2:
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
dev: false
/xtend/4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}

View File

@ -1,10 +1,33 @@
import { FC, HTMLProps } from "react";
import clsx from "clsx";
import { FC, MouseEventHandler, ReactNode } from "react";
import { InputSize } from "../../types";
const Button: FC<HTMLProps<HTMLButtonElement>> = (props) => {
interface InputProps {
onClick?: MouseEventHandler<HTMLButtonElement> | undefined;
className?: string;
children?: ReactNode;
size?: InputSize;
}
const getButtonSize = (size?: InputSize) => {
switch (size) {
case "md":
return "px-[1rem] py-[.5rem] text-sm";
default:
return "px-10 py-4 text-base";
}
};
const Button: FC<InputProps> = (props) => {
const btnClassName = getButtonSize(props.size);
return (
<button
onClick={props.onClick}
className="flex items-center justify-center w-full px-10 py-4 text-base font-medium text-center text-white transition duration-500 ease-in-out transform bg-blue-600 rounded-xl hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
className={clsx(
"flex items-center justify-center w-full font-medium text-center text-white transition duration-500 ease-in-out transform bg-blue-600 rounded-xl hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
btnClassName,
props.className
)}
>
{props.children}
</button>

View File

@ -0,0 +1,11 @@
import { FC, HTMLProps } from "react";
const InlineLink: FC<HTMLProps<HTMLAnchorElement>> = (props) => {
return (
<a className="text-sky-400 inline-block" href={props.href}>
{props.children}
</a>
);
};
export default InlineLink;

View File

@ -1,14 +1,44 @@
import { FC, HTMLProps } from "react";
import clsx from "clsx";
import { FC } from "react";
import { InputSize } from "../../types";
const Input: FC<HTMLProps<HTMLLabelElement>> = (props) => {
interface InputProps {
id?: string;
type?: string;
className?: string;
placeholder?: string;
required?: boolean;
name?: string;
size?: InputSize;
value?: string;
onInput?: React.FormEventHandler<HTMLInputElement>;
}
const getInputSize = (size?: InputSize) => {
switch (size) {
case "md":
return "px-[1rem] py-[.5rem] text-sm";
default:
return "px-5 py-3 text-base";
}
};
const Input: FC<InputProps> = (props) => {
const sizeClassNames = getInputSize(props.size);
return (
<input
id={props.id}
name={props.name}
type={props.type}
value={props.value}
onInput={props.onInput}
placeholder={props.placeholder}
required={props.required}
className="block w-full px-5 py-3 text-base text-neutral-600 placeholder-gray-300 transition duration-500 ease-in-out transform border border-transparent rounded-lg bg-gray-50 focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-300 shadow-sm"
className={clsx(
"block w-full text-neutral-600 placeholder-gray-300 transition duration-500 ease-in-out transform border border-transparent rounded-lg bg-gray-50 focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-300 shadow-sm",
sizeClassNames,
props.className
)}
/>
);
};

View File

@ -1,12 +1,17 @@
import { FC, HTMLProps } from "react";
const InputLabel: FC<HTMLProps<HTMLLabelElement>> = (props) => {
interface InputLabelProps extends HTMLProps<HTMLLabelElement> {
required?: boolean;
}
const InputLabel: FC<InputLabelProps> = (props) => {
return (
<label
htmlFor={props.id}
className="block text-sm font-medium text-gray-700"
>
{props.children}
{props.required && <span className="text-red-600">*</span>}
</label>
);
};

View File

@ -0,0 +1,7 @@
import { FC, HTMLProps } from "react";
const Container: FC<HTMLProps<HTMLDivElement>> = (props) => {
return <div className="w-full lg:w-1/2 mx-auto">{props.children}</div>;
};
export default Container;

View File

@ -7,6 +7,7 @@ import {
} from "@heroicons/react/outline";
import { FC, ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import useServices from "../../hooks/useServices";
import Badge from "../atoms/Badge";
import InputLabel from "../atoms/InputLabel";
@ -70,7 +71,7 @@ const Sidebar = () => {
<ul className="space-y-1">
<li>
<SidebarTab
badge={<Badge>0</Badge>}
badge={<Badge>12</Badge>}
onClick={navFactory("/services")}
>
<PuzzleIcon className="w-6 h-6 text-gray-700" />

View File

@ -1,17 +1,34 @@
import { FC, HTMLProps } from "react";
import { FC } from "react";
import { InputSize } from "../../types";
import Input from "../atoms/Input";
import InputLabel from "../atoms/InputLabel";
const TextField: FC<HTMLProps<HTMLInputElement>> = (props) => {
interface TextFieldProps {
id?: string;
type?: string;
className?: string;
placeholder?: string;
required?: boolean;
name?: string;
size?: InputSize;
label?: string;
inputClassName?: string;
}
const TextField: FC<TextFieldProps> = (props) => {
return (
<div>
<InputLabel id={props.id}>{props.label}</InputLabel>
<div className="w-full">
<InputLabel id={props.id} required={props.required}>
{props.label}
</InputLabel>
<div className="mt-1">
<Input
id={props.id}
type={props.type}
className={props.inputClassName}
placeholder={props.placeholder}
required={props.required}
size={props.size}
/>
</div>
</div>

View File

@ -1,9 +1,104 @@
import { PlusIcon } from "@heroicons/react/outline";
import clsx from "clsx";
import { useState } from "react";
import useServices from "../../../hooks/useServices";
import Button from "../../atoms/Button";
import InlineLink from "../../atoms/InlineLink";
import Container from "../../layouts/Container";
import DashboardLayout from "../../layouts/Dashboard";
import TextField from "../../molecules/TextField";
const ServicesPage = () => {
const [toggleCreate, setToggleCreate] = useState(false);
const services = useServices();
const handleToggleCreate = () => {
setToggleCreate(!toggleCreate);
};
return (
<DashboardLayout>
<div>Services</div>
<Container>
<h1 className="text-3xl font-bold mt-12 mb-8">Services</h1>
<div className="mb-4 flex items-end justify-between">
<TextField
label="Search"
size="md"
placeholder="Search services"
inputClassName="bg-white border border-gray-300 w-60 shadow-none"
/>
<Button
size="md"
className={clsx(
"w-20 border border-sky-600",
toggleCreate && "bg-white text-sky-600 hover:bg-white"
)}
onClick={handleToggleCreate}
>
<div className="flex flex-row space-x-1">
<PlusIcon width={20} />
<span>Add</span>
</div>
</Button>
</div>
{toggleCreate && (
<div className="w-full bg-gray-50 border border-gray-200 rounded-lg p-5 mb-4">
<h1 className="mb-4">Create a new service</h1>
<div className="space-y-3">
<TextField
label="Name"
required={true}
placeholder="service name"
size="md"
inputClassName="shadow-none border border-gray-300 bg-white"
/>
<TextField
label="Destination"
required={true}
placeholder="url"
size="md"
inputClassName="shadow-none border border-gray-300 bg-white"
/>
<div className="flex justify-end items-center">
<Button size="md" className="w-20 border border-sky-600">
Create
</Button>
</div>
</div>
</div>
)}
<div className="rounded-lg border">
<table className="w-full border-collapse">
<thead className="h-12">
<tr className="text-left font-semibold text-sm border-b bg-gray-50">
<th className="px-3 py-2 lg:w-2/12">Name</th>
<th className="px-3 py-2 lg:w-4/12">Destination</th>
<th className="px-3 py-2 lg:w-5/12">Referenced by</th>
<th className="px-3 py-2 lg:w-1/12">{/* Delete */}</th>
</tr>
</thead>
<tbody>
{services.data &&
services.data.map((service) => (
<tr key={service.name} className="h-12 border-b ">
<td className="px-3 py-2 truncate">{service.name}</td>
<td className="px-3 py-2 truncate">
<InlineLink href="https://example.com/">
{service.destination[0]}
</InlineLink>
</td>
<td className="px-3 py-2 truncate">
<InlineLink href="https://example.com/">
{service.references}
</InlineLink>
</td>
<td className="py-2 cursor-pointer text-sky-900">Delete</td>
</tr>
))}
</tbody>
</table>
</div>
</Container>
</DashboardLayout>
);
};

View File

@ -0,0 +1,19 @@
import { faker } from "@faker-js/faker";
import { useQuery } from "react-query";
import { Service } from "../types";
let services = Array(11)
.fill(0)
.map(() => ({
name: faker.name.lastName(),
references: [faker.name.firstName()],
destination: [faker.internet.ipv4()],
}));
const fetchServices = async (): Promise<Service[]> => {
return services;
};
export default function useServices() {
return useQuery(["services"], fetchServices);
}

View File

@ -1,10 +1,19 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import App from "./App";
import "./index.css";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
</React.StrictMode>
);
export { queryClient };

7
frontend/src/types.ts Normal file
View File

@ -0,0 +1,7 @@
export type InputSize = "sm" | "md" | "lg";
export interface Service {
name: string;
references: string[];
destination: string[];
}