Styles, API calls, and more

This commit is contained in:
Sean Edmunds
2024-08-13 17:08:16 -05:00
parent 722d8e65ec
commit 1e57c1a19e
34 changed files with 2560 additions and 643 deletions

1375
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,26 +10,36 @@
"preview": "vite preview"
},
"dependencies": {
"@iconify/react": "^4.1.1",
"@mantine/core": "^7.9.1",
"@mantine/hooks": "^7.9.1",
"@tanstack/react-query": "^5.36.0",
"jotai": "^2.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.2.1",
"react-query": "^3.39.3"
"@emotion/react": "^11.13.0",
"@iconify/react": "^5.0.2",
"@mantine/core": "^7.12.1",
"@mantine/dates": "^7.12.1",
"@mantine/form": "^7.12.1",
"@mantine/hooks": "^7.12.1",
"@mantine/modals": "^7.12.1",
"@tabler/icons-react": "^3.12.0",
"@tanstack/react-query": "^5.51.23",
"clsx": "^2.1.1",
"dayjs": "^1.11.12",
"jotai": "^2.9.3",
"mantine-react-table": "^2.0.0-beta.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-query": "^3.39.3",
"react-router-dom": "^6.26.0",
"react-toastify": "^10.0.5"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"typescript": "^5.2.2",
"vite": "^5.2.0"
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.9",
"typescript": "^5.5.4",
"vite": "^5.4.0"
}
}

View File

@@ -1,8 +1,6 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
width: 100%;
height: 100%;
}
.logo {

View File

@@ -2,6 +2,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import './App.css'
import Homepage from './pages/homepage/Homepage'
import { MantineProvider } from '@mantine/core'
import {
Route,
createBrowserRouter,
createRoutesFromElements,
RouterProvider,
Navigate,
} from "react-router-dom";
import Settings from './pages/settings/Settings';
function App() {
const queryClient = new QueryClient({
@@ -13,10 +21,21 @@ function App() {
},
});
const router = createBrowserRouter(
createRoutesFromElements(
<Route>
<Route path="/trigger" element={<Homepage />}></Route>
<Route path="/trigger/settings" element={<Settings />}></Route>
</Route>,
),
);
return (
<QueryClientProvider client={queryClient}>
<MantineProvider>
<Homepage />
<MantineProvider theme={{
fontFamily: '-apple-system,Jost,BlinkMacSystemFont,Helvetica Neue,Roboto,sans-serif',
}}>
<RouterProvider router={router} />
</MantineProvider>
</QueryClientProvider>
)

View File

@@ -0,0 +1,99 @@
import { ModalsProvider } from "@mantine/modals";
import { MantineReactTable, useMantineReactTable } from "mantine-react-table"
import { memo } from "react";
// import '../dataTableStyles.css';
import '@mantine/core/styles.css'; //import Mantine V7 styles needed by MRT
import '@mantine/dates/styles.css'; //if using mantine date picker features
import 'mantine-react-table/styles.css'; //import MRT styles
export function DataTableComponent({
columns,
tableData,
isFetching,
isLoading,
onCreatingRowSave,
onEditingRowSave,
}: any) {
const table = useMantineReactTable({
columns: columns,
data: tableData,
enableFacetedValues: true,
enablePagination: false,
enableRowVirtualization: true,
enableEditing: true,
createDisplayMode: "modal",
editDisplayMode: "row",
enableBottomToolbar: false,
enableToolbarInternalActions: false,
onCreatingRowSave: onCreatingRowSave,
onEditingRowSave: onEditingRowSave,
getRowId: (row: any) => row.id,
initialState: {
showProgressBars: isFetching,
showLoadingOverlay: isLoading,
isLoading: isLoading,
},
state: {
showProgressBars: isFetching,
showLoadingOverlay: isLoading,
isLoading: isLoading,
},
// mantineToolbarAlertBannerBadgeProps: {
// style: {
// color: "red",
// },
// },
// mantineTopToolbarProps: {
// style: {
// color: "var(--color-text-pri)",
// background: "var(--color-background)",
// border: "none",
// boxShadow: "none",
// },
// },
// mantineTableHeadProps: {
// style: {
// boxShadow: "none",
// },
// },
// mantineTableHeadCellProps: {
// style: {
// color: "var(--color-text-pri)",
// background: "var(--color-background)",
// borderRight: "none",
// borderLeft: "none",
// },
// },
// mantineTableBodyCellProps: {
// style: {
// background: "var(--color-background)",
// borderRight: "none",
// borderLeft: "none",
// },
// },
// mantinePaperProps: {
// style: {
// border: "none",
// boxShadow: "none",
// background: "var(--color-background)",
// },
// },
// mantineTableContainerProps: {
// style: {
// border: "none",
// boxShadow: "none",
// background: "var(--color-background)",
// },
// },
});
return (
<ModalsProvider>
<MantineReactTable table={table} />
</ModalsProvider>
);
}
export const DataTable = memo(DataTableComponent);
export default DataTable;

View File

@@ -0,0 +1,117 @@
import { MRT_ColumnDef } from "mantine-react-table";
import { useMemo } from "react";
export default function ApplicationColumns() {
const columns = useMemo<MRT_ColumnDef<any>[]>(
() => [
{
accessorFn: (row) => {
if (row.id) {
return `${row.id}`;
} else {
return "";
}
},
enableEditing: false,
id: "id",
header: "ID",
size: 50,
mantineCopyButtonProps: ({ cell, row }) => ({
className: "id",
id: `${cell.getValue()}-id`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.name) {
return `${row.name}`;
} else {
return "";
}
},
id: "name",
header: "Name",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "name",
id: `${cell.getValue()}-name`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.url) {
return `${row.url}`;
} else {
return "";
}
},
id: "url",
header: "Url",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "url",
id: `${cell.getValue()}-url`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.icon) {
return `${row.icon}`;
} else {
return "";
}
},
id: "icon",
header: "Icon",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "icon",
id: `${cell.getValue()}-icon`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.urlbase) {
return `${row.urlbase}`;
} else {
return "";
}
},
id: "urlbase",
header: "Base Url",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "urlbase",
id: `${cell.getValue()}-urlbase`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.category) {
return `${row.category}`;
} else {
return "";
}
},
id: "category",
header: "Category",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "category",
id: `${cell.getValue()}-category`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
],
[],
);
return columns;
}

View File

@@ -0,0 +1,87 @@
import { MRT_ColumnDef } from "mantine-react-table";
import { useMemo } from "react";
export default function ApplicationColumns(groups: any, groupData: any) {
const columns = useMemo<MRT_ColumnDef<any>[]>(
() => [
{
accessorFn: (row) => {
if (row.id) {
return `${row.id}`;
} else {
return "";
}
},
enableEditing: false,
id: "id",
header: "ID",
size: 50,
mantineCopyButtonProps: ({ cell, row }) => ({
className: "id",
id: `${cell.getValue()}-id`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.name) {
return `${row.name}`;
} else {
return "";
}
},
id: "name",
header: "Name",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "name",
id: `${cell.getValue()}-name`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.url) {
return `${row.url}`;
} else {
return "";
}
},
id: "url",
header: "Url",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "url",
id: `${cell.getValue()}-url`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
const index: number = row.category;
if (index !== null && index !== undefined) {
return `${index}`;
} else {
return "N/A";
}
},
id: "category",
header: "Category",
editVariant: "select",
mantineEditSelectProps: { data: groups },
mantineCopyButtonProps: ({ cell, row }) => ({
className: "category",
id: `${cell.getValue()}-category`,
}),
Cell: ({ renderedCellValue, row }) =>
groupData[row?.original.category].category_name,
Header: ({ column }) => column.columnDef.header,
},
],
[groups],
);
return columns;
}

View File

@@ -0,0 +1,66 @@
import { MRT_ColumnDef } from "mantine-react-table";
import { useMemo } from "react";
export default function MessagesColumns() {
const columns = useMemo<MRT_ColumnDef<any>[]>(
() => [
{
accessorFn: (row) => {
if (row.id) {
return `${row.id}`;
} else {
return "";
}
},
enableEditing: false,
id: "id",
header: "ID",
size: 50,
mantineCopyButtonProps: ({ cell, row }) => ({
className: "id",
id: `${cell.getValue()}-id`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.message) {
return `${row.message}`;
} else {
return "";
}
},
id: "message",
header: "Message",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "message",
id: `${cell.getValue()}-message`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row) => {
if (row.easteregg) {
return `${row.easteregg}`;
} else {
return "";
}
},
id: "easteregg",
header: "Easter Egg?",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "easteregg",
id: `${cell.getValue()}-easteregg`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
],
[],
);
return columns;
}

View File

@@ -0,0 +1,65 @@
import { MRT_ColumnDef } from "mantine-react-table";
import { useMemo } from "react";
export default function PersonalizationColumns() {
const columns = useMemo<MRT_ColumnDef<any>[]>(
() => [
{
accessorFn: (row: any) => {
if (row.firstname) {
return `${row.firstname}`;
} else {
return "";
}
},
id: "firstname",
header: "First Name",
size: 50,
mantineCopyButtonProps: ({ cell, row }) => ({
className: "firstname",
id: `${cell.getValue()}-firstname`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row: any) => {
if (row.lastname) {
return `${row.lastname}`;
} else {
return "";
}
},
id: "lastname",
header: "Last Name",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "lastname",
id: `${cell.getValue()}-lastname`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row: any) => {
if (row.email) {
return `${row.email}`;
} else {
return "";
}
},
id: "email",
header: "Email",
mantineCopyButtonProps: ({ cell, row }) => ({
className: "email",
id: `${cell.getValue()}-email`,
}),
Cell: ({ renderedCellValue, row }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
],
[],
);
return columns;
}

View File

@@ -0,0 +1,16 @@
import { Stack, Text } from '@mantine/core';
import { stackClasses } from '../styles';
export function ToastError({ err }: { err: any }) {
const errorResponse = JSON.parse(err.message);
return (
<Stack className={stackClasses.VStack} style={{ alignItems: 'flex-start' }}>
<Text>
Status: {errorResponse.status} {errorResponse.statusText}
</Text>
<Text>Details: {errorResponse.msg}</Text>
{errorResponse.loc && <Text>Location: {errorResponse.loc}</Text>}
{errorResponse.type && <Text>Type: {errorResponse.type}</Text>}
</Stack>
);
}

198
src/dataTableStyles.css Normal file
View File

@@ -0,0 +1,198 @@
.mrt-table-paper {
height: 100%;
overflow: visible;
background-color: transparent;
border: none;
border-style: none;
box-shadow: none;
&.mantine-Overlay-root {
color: var(--mantine-primary-color-7);
background-color: alpha(var(--mantine-color-appGray-9), 0.5);
}
&.mrt-toolbar-dropzone {
height: 4.5vh;
width: 35vw;
color: var(--mantine-primary-color-6);
font-size: 36px;
background-color: rgb(255, 255, 255, 1);
justify-self: center;
&.inner {
font-size: 36px;
}
}
& ::-webkit-scrollbar {
width: 8px;
height: 8px;
border-radius: 5px;
background-color: transparent;
}
& ::-webkit-scrollbar-thumb:hover {
background-color: var(--mantine-primary-color-4);
}
& ::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 5px;
}
& ::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: var(--mantine-primary-color-6);
}
& ::-webkit-scrollbar-corner {
background-color: light-dark(white, var(--mantine-primary-color-8));
}
}
.mrt-table-container {
height: 100%;
background-color: light-dark(white, var(--mantine-primary-color-8));
border-top: solid;
border-top-color: light-dark(
var(--mantine-color-appGray-1),
var(--mantine-primary-color-6)
);
border-top-width: 1px;
border-left: solid;
border-left-color: light-dark(
var(--mantine-color-appGray-1),
var(--mantine-primary-color-6)
);
border-left-width: 1px;
border-right: solid;
border-right-color: light-dark(
var(--mantine-color-appGray-1),
var(--mantine-primary-color-6)
);
border-right-width: 1px;
border-bottom: none;
border-radius: 4px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.mantine-Table-th {
color: light-dark(var(--mantine-color-appGray-7), var(--mantine-primary-color-3));
background-color: light-dark(white, var(--mantine-primary-color-8));
border-color: light-dark(
var(--mantine-color-appGray-1),
var(--mantine-primary-color-7)
);
/* & .mantine-ActionIcon-root {
margin-right: 1px;
display: none;
} */
& .mrt-table-head-cell-content {
padding-top: 12px;
padding-bottom: 12px;
align-items: center;
justify-content: center;
white-space: nowrap;
.mrt-table-head-cell-labels {
& > * > .mantine-Text-root {
font-size: 14px;
font-weight: bold;
}
}
& .mrt-table-head-cell-content-actions {
transition: 1s all ease;
& .mantine-ActionIcon-root {
color: transparent;
background-color: transparent;
display: hidden;
}
}
}
& .mrt-table-head-cell-resize-handle {
display: flex;
width: 4px;
border-radius: 4px;
border-left-color: transparent;
background-color: light-dark(
var(--mantine-primary-color-1),
var(--mantine-primary-color-6)
);
}
& .mantine-TableHeadCell-Content-Labels {
cursor: pointer;
position: absolute;
}
&:hover {
& .mrt-table-head-cell-content-actions {
& .mantine-ActionIcon-root {
color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-3));
background-color: light-dark(
var(--mantine-primary-color-1),
var(--mantine-primary-color-6)
);
display: grid;
position: absolute;
right: 0;
}
}
& .mrt-table-head-cell-resize-handle {
border-left-color: var(--mantine-primary-color-2);
border-width: 4px;
border-left-style: solid;
}
}
}
.mantine-Mark-root {
background-color: light-dark(
var(--mantine-primary-color-7),
var(--mantine-primary-color-7)
);
color: white;
}
.mantine-Table-tr {
height: 56px;
box-shadow: none;
& .table-icon-device-floppy {
color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-3));
}
}
.mantine-Table-td {
color: light-dark(var(--mantine-color-appGray-7), var(--mantine-primary-color-3));
justify-content: center;
border-color: light-dark(
var(--mantine-color-appGray-1),
var(--mantine-primary-color-7)
);
& > * > .mantine-Text-root {
font-size: 14px;
}
}
.mrt-table-head-sort-button {
display: none;
}
.mrt-table-head-sort-button[data-sorted='asc'] {
display: flex;
min-height: 12px !important;
min-width: 12px !important;
max-width: 16px;
max-height: 16px;
& > * > .tabler-icon-arrows-sort {
height: 12px !important;
width: 12px !important;
color: light-dark(--mantine-primary-color-6, --mantine-color-appGray-5);
}
}
.mrt-table-head-sort-button[data-sorted='desc'] {
display: flex;
min-height: 12px !important;
min-width: 12px !important;
max-width: 16px;
max-height: 16px;
& > * > .tabler-icon-arrows-sort {
height: 12px !important;
width: 12px !important;
color: light-dark(--mantine-primary-color-6, --mantine-color-appGray-5);
}
}

View File

@@ -1,7 +1,9 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQuery, useQueryClient, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { toast } from 'react-toastify';
export function useQueryAPI({ endpoint }: { endpoint: string }) {
const queryClient = useQueryClient();
const callEndpoint = async (endpoint: string) => {
const url = `http://localhost:5000/trigger/${endpoint}`;
const options: object = {
@@ -16,22 +18,425 @@ export function useQueryAPI({ endpoint }: { endpoint: string }) {
'Sec-Fetch-Site': 'same-site',
},
};
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
const responseData = await response.json();
return responseData;
return await response.json();
};
return useQuery({
queryKey: ['GET', endpoint],
queryFn: () => callEndpoint(endpoint),
retry: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: 30 * (60 * 1000), // 30 minutes
// cacheTime: 60 * (60 * 1000), // 60 minutes
});
}
export function useMutationAPI() {
const callEndpoint = async (
method: string,
endpoint: string,
body: object | undefined | null,
) => {
const url = `http://localhost:5000/trigger/${endpoint}`;
const options: any = {
method: method,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Credentials': true,
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'same-site'
},
body: JSON.stringify(body),
};
const response = await fetch(url, options);
if (!response.ok) {
const responseData = await response.json();
if (typeof responseData.detail !== 'string') {
const errorResponse = JSON.stringify({
status: response.status,
statusText: response.statusText,
method: options.method,
endpoint: endpoint,
msg: responseData.detail[0]?.msg,
loc: responseData.detail[0]?.loc,
type: responseData.detail[0]?.type,
});
throw new Error(errorResponse);
}
if (typeof responseData.detail === 'object') {
const errorResponse = JSON.stringify({
status: response.status,
statusText: response.statusText,
method: options.method,
endpoint: endpoint,
msg: responseData.detail.message,
});
throw new Error(errorResponse);
}
const errorResponse = JSON.stringify({
status: response.status,
statusText: response.statusText,
method: options.method,
endpoint: endpoint,
msg: responseData.detail,
});
throw new Error(errorResponse);
}
return await response.json();
};
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
method,
endpoint,
body = undefined,
}: {
method: string;
endpoint: string;
body?: object | undefined | null;
}) => callEndpoint(method, endpoint, body),
onMutate: () => {},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ['GET', variables.endpoint] });
if (variables.endpoint.includes('/')) {
if ((variables.endpoint.match(/\//g) || []).length > 2) {
const GetEndpoint = variables.endpoint.split('/');
const Endpoint = `${GetEndpoint[1]}/${GetEndpoint[2]}`;
queryClient.invalidateQueries({ queryKey: ['GET', `/${Endpoint}`] });
} else {
const GetEndpoint = variables.endpoint.split('/')[1];
queryClient.invalidateQueries({ queryKey: ['GET', `/${GetEndpoint}`] });
}
}
},
});
}
export function useFileUploadAPI() {
const callEndpoint = async (
method: string,
endpoint: string,
body?: object | FormData | undefined | null,
) => {
const url = `http://localhost:5000/trigger/${endpoint}`;
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Credentials': true,
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'same-site',
},
body: body ?? undefined,
};
const response = await fetch(url, options as unknown as Body);
if (!response.ok) {
const responseData = await response.json();
if (typeof responseData.detail !== 'string') {
const errorResponse = JSON.stringify({
status: response.status,
statusText: response.statusText,
method: options.method,
endpoint: endpoint,
msg: responseData.detail[0].msg,
loc: responseData.detail[0].loc,
type: responseData.detail[0].type,
});
throw new Error(errorResponse);
}
const errorResponse = JSON.stringify({
status: response.status,
statusText: response.statusText,
method: options.method,
endpoint: endpoint,
msg: responseData.detail,
});
throw new Error(errorResponse);
}
return await response.json();
};
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
method,
endpoint,
body,
}: {
method: string;
endpoint: string;
body: object | FormData | undefined | null;
toastID?: string;
}) => callEndpoint(method, endpoint, body),
onMutate: () => {},
onSuccess: (variables) => {
queryClient.invalidateQueries({ queryKey: ['GET', variables.endpoint] });
if (variables.endpoint?.includes('/')) {
const GetEndpoint = variables.endpoint.split('/')[1];
queryClient.invalidateQueries({ queryKey: ['GET', `/${GetEndpoint}`] });
}
},
});
}
export function useFetchAllRecordsAPI({ endpoint }: { endpoint: string }) {
const pageSize = 500;
const queryClient = useQueryClient();
const fetchPage = async (page: number, retryCount = 0): Promise<any> => {
// Start building the URL
const url = new URL(`http://localhost:5000/trigger/${endpoint}`);
// Check and append custom query parameters from the endpoint
const customParams = new URLSearchParams(endpoint.split('?')[1]);
for (const [key, value] of customParams) {
if (!url.searchParams.has(key)) {
url.searchParams.append(key, value);
}
}
// Add 'page' and 'size' parameters
url.searchParams.set('page', page.toString()); // Use 'set' to ensure uniqueness
url.searchParams.set('size', pageSize.toString());
const options: any = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Credentials': true,
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'same-site',
},
};
try {
const response = await fetch(url.toString(), options);
if (!response.ok) {
// If the response is not ok and status is 401 (not authorized), reload the page
if (response.status === 401) {
window.location.reload();
}
throw new Error(
`Error fetching page ${page}: ${response.status} ${response.statusText}`,
);
}
return response.json();
} catch (err) {
if (retryCount < 3) {
// Retry up to 3 times with a random delay
const delay = Math.floor(Math.random() * 3000) + 1000; // Random delay between 1 to 3 seconds
console.error(
`Retrying page ${page}, attempt ${retryCount + 1} after ${delay} ms`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchPage(page, retryCount + 1);
}
console.error(`Error fetching page ${page} after 3 retries:`, err);
throw err; // Re-throw the error to handle it in the calling function
}
};
const callEndpoint = async () => {
try {
// Fetch the first page to get the total number of items and pages
const firstPageData = await fetchPage(1);
// If the first page doesn't have a 'pages' property, return the data as-is
if (firstPageData?.pages === undefined) {
return firstPageData;
}
if (firstPageData?.pages === 1) {
return firstPageData.items;
}
let totalPages = firstPageData.pages;
const allItems = [...firstPageData.items];
// Fetch each page sequentially
for (let page = 2; page <= totalPages; page++) {
try {
const pageData = await fetchPage(page);
allItems.push(...pageData.items);
// Check and update the totalPages if it has increased
if (pageData.pages > totalPages) {
totalPages = pageData.pages;
}
} catch (err) {
console.error(`Final error fetching page ${page}:`, err);
// Continue to the next page even if the current page fails
}
}
return allItems;
} catch (error) {
// Handling errors in fetching the first page
console.error('Error fetching the first page:', error);
return []; // Return empty array on error
}
};
const invalidate = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ['GET', endpoint] });
}, [queryClient, endpoint]);
return {
...useQuery({
queryKey: ['GET', endpoint],
queryFn: callEndpoint,
}),
invalidate,
};
}
interface DownloadFileOptions {
endpoint: string;
file_name: string;
}
export function useDownloadFile() {
const downloadFile = useCallback(
async ({ endpoint, file_name }: DownloadFileOptions) => {
const url = `http://localhost:5000/trigger/${endpoint}`;
const options: any = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Credentials': true,
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'same-site',
},
};
const toastID = toast.loading('Processing...');
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const blob = await response.blob();
if (blob.type === 'application/json') {
const blob_text = await blob.text();
const cleaned_text = blob_text.replace(/"/g, '');
toast.update(toastID, {
render: cleaned_text,
type: 'info',
isLoading: false,
autoClose: 5000,
closeButton: null,
});
return;
}
if (typeof window !== 'undefined') {
const blobURL = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = blobURL;
a.download = file_name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(blobURL);
document.body.removeChild(a);
toast.update(toastID, {
render: `${file_name} Downloaded`,
type: 'success',
isLoading: false,
autoClose: 5000,
closeButton: null,
});
}
} catch (error) {
console.error('Download failed:', error);
toast.update(toastID, {
render: `Error Downloading ${file_name}. Please try again.`,
type: 'error',
isLoading: false,
autoClose: 5000,
closeButton: null,
});
}
},
[],
);
return downloadFile;
}
export const useFetchInfiniteQuery = ({
endpoint,
pageSize = 10,
}: {
endpoint: string;
pageSize?: number;
}) => {
const queryKey = ['GET', endpoint];
const fetchInfiniteQuery = async (endpoint: string) => {
const url = `http://localhost:5000/trigger/${endpoint}`;
const requestOptions: any = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Credentials': true,
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'same-site'
},
};
try {
const response = await fetch(url, requestOptions);
return await response.json();
} catch (error) {
console.error('Error fetching transactions:', error);
throw error;
}
};
const queryFn = ({ pageParam = 1 }) =>
fetchInfiniteQuery(`${endpoint}&page=${pageParam}&size=${pageSize}`);
const getNextPageParam = (lastPage: any, allPages: any) => {
if (lastPage.items.length < pageSize) {
return undefined;
}
return allPages.length + 1;
};
const { data, isLoading, isFetchingNextPage, fetchNextPage, isError, isFetching } =
useInfiniteQuery({
queryKey,
queryFn,
getNextPageParam,
initialPageParam: 1,
});
return { data, isLoading, isFetchingNextPage, fetchNextPage, isError, isFetching };
};

View File

@@ -1,3 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100;0,200;0,400;1,100;1,200&display=swap');
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
@@ -54,6 +56,14 @@ button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
#settings-container {
justify-content: left;
}
#settings-nav {
width: 25vw;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;

View File

@@ -1,5 +1,4 @@
import { Stack } from '@mantine/core';
import { useEffect, useState } from 'react';
import stackClasses from '../../styles/Stack.module.css';
import {useQueryAPI} from '../../hooks/useAPI';
import Search from './components/Search';
@@ -9,6 +8,7 @@ import Bookmarks from './components/Bookmarks';
import SettingsButton from './components/SettingsButton';
import { useAtom } from 'jotai';
import { HomepageFilters } from '../../contexts/HomepageFilters';
import { useEffect, useState } from 'react';
export default function Homepage() {

View File

@@ -35,9 +35,10 @@ function AppItem({ url, icon, name }: any) {
gap: 0,
}}
>
<Text className={texts.appName}>{name}</Text>
<Text className={texts.appName} classNames={texts}>{name}</Text>
<Text
className={texts.appUrl}
classNames={texts}
style={{
textDecoration: textDecoration,
}}

View File

@@ -16,10 +16,10 @@ function Applications() {
return (
<Stack id='apps' style={{ gap: 0 }}>
<Text className={texts.sectionTitle}>Applications</Text>
<Text className={texts.sectionTitle} classNames={texts}>Applications</Text>
<Stack
style={{
// background: 'var(--color-background)',
background: 'var(--color-background)',
gap: '.25rem',
backgroundSize: 'cover',
borderBottom: '0px solid',

View File

@@ -25,7 +25,7 @@ function Links() {
<>
{groupsIsFetched && linksIsFetched && categories?.length > 0 && (
<Stack id='links' style={{ gap: 0 }}>
<Text className={texts.sectionTitle}>Bookmarks</Text>
<Text className={texts.sectionTitle} classNames={texts}>Bookmarks</Text>
<Stack
style={{
display: 'grid',

View File

@@ -11,7 +11,7 @@ function LinkGroups({ links, category }: any) {
lineHeight: '1.5rem',
}}
>
<Text className={texts.linkCategory}>{category}</Text>
<Text className={texts.linkCategory} classNames={texts}>{category}</Text>
{links?.map((link: any) => (
<LinkItem key={link.name} link={link} />
))}

View File

@@ -3,7 +3,7 @@ import texts from '../../../styles/Text.module.css';
function LinkItem({ link }: any) {
return (
<Text className={texts.linkItem} component='a' href={link.url}>
<Text className={texts.linkItem} classNames={texts} component='a' href={link.url}>
{link.name}
</Text>
);

View File

@@ -24,7 +24,9 @@ function SettingsButton() {
useEffect(() => {
if(data) {
setCategories(data.map((category: any) => {return {value: category.category, label: category.category}}));
let list: [any] = data.map((category: any) => {return {value: category.category, label: category.category}}) || [];
list.unshift({value: 'All', label: 'All'});
setCategories(list);
}
}, [data, isFetched]);
@@ -41,7 +43,9 @@ function SettingsButton() {
withArrow
shadow='md'
position='top'
classNames={menu}
classNames={{
dropdown: menu.dropdown
}}
styles={{
dropdown: {
backgroundColor: 'var(--color-background)',
@@ -49,7 +53,7 @@ function SettingsButton() {
}}
>
<Menu.Target>
<Button className={button.settingsButton}>
<Button className={button.settingsButton} classNames={button}>
<FiMenu />
</Button>
</Menu.Target>

View File

@@ -0,0 +1,38 @@
import { ActionIcon, Group, Stack, Tabs } from "@mantine/core";
import tabClasses from '../../styles/Tabs.module.css';
import Personalization from "./components/Personalization";
import { FiArrowLeftCircle } from "react-icons/fi";
import { actionIconClasses } from "../../styles";
import Messages from "./components/Messages";
import Applications from "./components/Applications";
import Bookmarks from "./components/Bookmarks";
function Settings() {
return (
<Stack h={'100vh'} px={'2rem'}>
<Group gap={0} >
<ActionIcon className={actionIconClasses.backButton} classNames={actionIconClasses} component="a" href="/trigger"><FiArrowLeftCircle/></ActionIcon>
<h1>Settings</h1>
</Group>
<Tabs keepMounted orientation="vertical" defaultValue={'personalization'} className={tabClasses.settingsTabs} classNames={tabClasses}>
<Tabs.List>
<Tabs.Tab value="personalization">Personal Info</Tabs.Tab>
<Tabs.Tab value="messages">Messages</Tabs.Tab>
<Tabs.Tab value="applications">Applications</Tabs.Tab>
<Tabs.Tab value="bookmarksCategories">Bookmark Categories</Tabs.Tab>
<Tabs.Tab value="bookmarks">Bookmarks</Tabs.Tab>
<Tabs.Tab value="themes">Themes</Tabs.Tab>
<Tabs.Tab value="providers">Search Providers</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="personalization"><Personalization/></Tabs.Panel>
<Tabs.Panel value="messages"><Messages/></Tabs.Panel>
<Tabs.Panel value="applications"><Applications/></Tabs.Panel>
<Tabs.Panel value="bookmarksCategories">Bookmark Categories</Tabs.Panel>
<Tabs.Panel value="bookmarks"><Bookmarks/></Tabs.Panel>
<Tabs.Panel value="themes">Themes</Tabs.Panel>
<Tabs.Panel value="providers">Search Providers</Tabs.Panel>
</Tabs>
</Stack>
);
}
export default Settings;

View File

@@ -0,0 +1,59 @@
import { useEffect, useState } from "react";
import {useMutationAPI, useQueryAPI} from "../../../hooks/useAPI";
import { DataTable } from "../../../DataTable/DataTable";
import { toast } from "react-toastify";
import { MRT_TableOptions } from "mantine-react-table";
import ApplicationColumns from "../../../DataTable/columns/ApplicationColumns";
import { Stack } from "@mantine/core";
const ApplicationSettings = () => {
const endpoint = "api/apps";
const { data, isFetching, isLoading, isFetched } = useQueryAPI({
endpoint: endpoint,
});
const [applicationData, setApplicationData] = useState<any>([]);
const {mutateAsync: createRow, isPending: isCreatingRow} = useMutationAPI();
const {mutateAsync: editRow, isPending: isEditingRow} = useMutationAPI();
useEffect(() => {
if(isFetched) {
setApplicationData(data);
}
})
const handleCreateRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
exitCreatingMode,
}) => {
createRow({method: "POST", endpoint: endpoint, body: values}).then(() => {
toast.success("Application created successfully");
})
exitCreatingMode();
};
const handleEditRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
table,
}) => {
editRow({method: "PATCH", endpoint: endpoint, body: values}).then(() => {
toast.success("Application updated successfully");
});
table.setEditingRow(null);
};
return (
<Stack>
<h2>Applications</h2>
<DataTable
tableName="Applications"
columns={ApplicationColumns()}
tableData={applicationData}
isFetching={isFetching}
isLoading={isLoading}
onCreatingRowSave={handleCreateRow}
onEditingRowSave={handleEditRow}
/>
</Stack>
)
};
export default ApplicationSettings;

View File

@@ -0,0 +1,76 @@
import { useEffect, useState } from "react";
import {useMutationAPI, useQueryAPI} from "../../../hooks/useAPI";
import { DataTable } from "../../../DataTable/DataTable";
import { toast } from "react-toastify";
import { MRT_TableOptions } from "mantine-react-table";
import { Stack } from "@mantine/core";
import BookmarkColumns from "../../../DataTable/columns/BookmarkColumns";
const BookmarkSettings = () => {
const endpoint = "api/links";
const { data, isFetching, isLoading, isFetched } = useQueryAPI({
endpoint: endpoint,
});
const {
data: groupData,
isFetching: groupDataisFetching,
isLoading: groupDataisLoading,
isFetched: groupDataisFetched,
} = useQueryAPI({ endpoint: "api/categories" });
const [bookmarkData, setBookmarkData] = useState<any>([]);
const [groups, setGroups] = useState([]);
const [fullyFetched, setFullyFetched] = useState(false);
const {mutateAsync: createRow, isPending: isCreatingRow} = useMutationAPI();
const {mutateAsync: editRow, isPending: isEditingRow} = useMutationAPI();
useEffect(() => {
if(isFetched) {
setBookmarkData(data);
}
})
useEffect(() => {
if (groupDataisFetched) {
let groupList = groupData.map((category: any) => {
return category.category_name;
});
setGroups(groupList);
setFullyFetched(true);
}
}, [groupDataisFetched]);
const handleCreateRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
exitCreatingMode,
}) => {
createRow({method: "POST", endpoint: endpoint, body: values}).then(() => {
toast.success("Application created successfully");
})
exitCreatingMode();
};
const handleEditRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
table,
}) => {
editRow({method: "PATCH", endpoint: endpoint, body: values}).then(() => {
toast.success("Application updated successfully");
});
table.setEditingRow(null);
};
return (
<Stack>
<h2>Bookmarks</h2>
<DataTable
tableName="Applications"
columns={BookmarkColumns(groups, groupData)}
tableData={bookmarkData}
isFetching={isFetching}
isLoading={isLoading}
onCreatingRowSave={handleCreateRow}
onEditingRowSave={handleEditRow}
/>
</Stack>
)
};
export default BookmarkSettings;

View File

@@ -0,0 +1,59 @@
import { useEffect, useState } from "react";
import {useMutationAPI, useQueryAPI} from "../../../hooks/useAPI";
import { DataTable } from "../../../DataTable/DataTable";
import { toast } from "react-toastify";
import { MRT_TableOptions } from "mantine-react-table";
import MessagesColumns from "../../../DataTable/columns/MessagesColumns";
import { Stack } from "@mantine/core";
const MessageSettings = () => {
const endpoint = "api/messages";
const { data, isFetching, isLoading, isFetched } = useQueryAPI({
endpoint: endpoint,
});
const [messageData, setMessageData] = useState<any>([]);
const {mutateAsync: createRow, isPending: isCreatingRow} = useMutationAPI();
const {mutateAsync: editRow, isPending: isEditingRow} = useMutationAPI();
useEffect(() => {
if(isFetched) {
setMessageData(data);
}
})
const handleCreateRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
exitCreatingMode,
}) => {
createRow({method: "POST", endpoint: endpoint, body: values}).then(() => {
toast.success("Message created successfully");
})
exitCreatingMode();
};
const handleEditRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
table,
}) => {
editRow({method: "PATCH", endpoint: endpoint, body: values}).then(() => {
toast.success("Message updated successfully");
});
table.setEditingRow(null);
};
return (
<Stack>
<h2>Messages</h2>
<DataTable
tableName="Messages"
columns={MessagesColumns()}
tableData={messageData}
isFetching={isFetching}
isLoading={isLoading}
onCreatingRowSave={handleCreateRow}
onEditingRowSave={handleEditRow}
/>
</Stack>
)
};
export default MessageSettings;

View File

@@ -0,0 +1,58 @@
import { useEffect, useState } from "react";
import {useMutationAPI, useQueryAPI} from "../../../hooks/useAPI";
import { DataTable } from "../../../DataTable/DataTable";
import PersonalizationColumns from "../../../DataTable/columns/PersonalizationColumns";
import { toast } from "react-toastify";
import { MRT_TableOptions } from "mantine-react-table";
const PersonalizationSettings = () => {
const endpoint = "api/personaldata";
const { data, isFetching, isLoading, isFetched } = useQueryAPI({
endpoint: endpoint,
});
const [personalData, setPersonalData] = useState<any>([]);
const {mutateAsync: createRow, isPending: isCreatingRow} = useMutationAPI();
const {mutateAsync: editRow, isPending: isEditingRow} = useMutationAPI();
useEffect(() => {
if(isFetched) {
setPersonalData(data);
}
})
const handleCreateRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
exitCreatingMode,
}) => {
createRow({method: "POST", endpoint: endpoint, body: values}).then(() => {
toast.success("Personalization created successfully");
})
exitCreatingMode();
};
const handleEditRow: MRT_TableOptions<any>['onCreatingRowSave'] = async ({
values,
table,
}) => {
editRow({method: "PATCH", endpoint: endpoint, body: values}).then(() => {
toast.success("Personalization updated successfully");
});
table.setEditingRow(null);
};
return (
<>
<h2>Personalization</h2>
<DataTable
tableName="Personalization"
columns={PersonalizationColumns()}
tableData={personalData}
isFetching={isFetching}
isLoading={isLoading}
onCreatingRowSave={handleCreateRow}
onEditingRowSave={handleEditRow}
/>
</>
)
};
export default PersonalizationSettings;

View File

View File

@@ -1,6 +1,12 @@
.root {
background-color: transparent;
}
.icon {
color: var(--color-text-acc)
.backButton {
&.root {
background-color: transparent;
color: black;
width: 2rem;
height: 2rem;
.icon {
font-size: 24px;
}
}
}

View File

@@ -1,26 +1,30 @@
.settingsButton {
width: 2rem;
height: 2rem;
padding: 0px;
margin: 0px;
position: fixed;
bottom: 10px;
left: 10px;
color: var(--color-text-pri);
background-color: var(--color-background);
border-radius: 100%;
border: solid 2px var(--color-text-pri);
cursor: pointer;
&.root {
width: 2rem;
height: 2rem;
padding: 0px;
margin: 0px;
position: fixed;
bottom: 10px;
left: 10px;
color: var(--color-text-pri);
background-color: var(--color-background);
border-radius: 100%;
border: solid 2px var(--color-text-pri);
cursor: pointer;
}
}
.themeButton{
font-size: 1em;
font-weight:500;
margin: 10px;
width:128px;
line-height: 3rem;
text-align: center;
text-transform: uppercase;
border-radius: 1em;
height: 3.5rem;
&.root {
font-size: 1em;
font-weight:500;
margin: 10px;
width:128px;
line-height: 3rem;
text-align: center;
text-transform: uppercase;
border-radius: 1em;
height: 3.5rem;
}
}

View File

@@ -1,5 +1,7 @@
.dropdown {
background-color: var(--color-background);
border: solid 2px var(--color-border);
border-color: var(--color-border);
.settingsButton {
.dropdown {
background-color: white;
border: solid 2px var(--color-border);
border-color: var(--color-border);
}
}

View File

@@ -0,0 +1,6 @@
.settingsTabs {
.panel {
padding-left: 2rem;
height: 100%;
}
}

View File

@@ -1,90 +1,106 @@
.greeting {
margin: 0;
margin-left: .5em;
margin-right: .5em;
padding: 0;
text-align: left;
font-size: 4em;
font-weight: 400;
width: max-content;
color: var(--color-text-acc);
}
.dateTime {
display: flex;
font-size: 2em;
height: 30px;
margin: 0;
padding: 0;
margin-right: 1em;
text-transform: uppercase;
font-weight: 300;
color: var(--color-text-pri);
}
.sectionTitle {
text-decoration: underline;
font-size: 20px;
font-weight: 200;
font-style: italic;
height: 10px;
text-transform: uppercase;
margin-top: 20px;
margin-bottom: 20px;
color: var(--color-text-pri);
}
.appName {
font-weight: 400;
font-size: 0.8em;
font-style: normal;
color: light-dark(blue, red);
font-family: 'Jost', sans-serif;
margin: 0;
}
.appUrl {
font-size: 0.8em;
font-weight: 400;
font-style: italic;
text-transform: uppercase;
color: var(--color-text-acc);
margin: 0;
}
.linkCategory {
font-size: 1.1em;
font-weight: 400;
height: 10px;
color: var(--color-text-acc);
text-transform: uppercase;
margin-top: 20px;
margin-bottom: 20px;
}
.linkItem {
display: block;
font-size: 1em;
line-height: 2;
color: var(--color-text-pri)
}
.settingsLink {
font-size: 1.25em;
display: block;
margin-left: 0;
margin-right: 0;
line-height: 2;
justify-self: center;
color: var(--color-text-pri);
border-radius: 1em;
&:hover {
background-color: var(--color-text-acc);
&.root {
margin: 0;
margin-left: .5em;
margin-right: .5em;
padding: 0;
text-align: left;
font-size: 4em;
font-weight: 400;
width: max-content;
color: var(--color-text-acc);
}
}
.linkItem:hover {
text-decoration: underline;
.dateTime {
&.root {
display: flex;
font-family: -apple-system, Jost, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif !important;
font-size: 2em;
height: 30px;
margin: 0;
padding: 0;
margin-right: 1em;
text-transform: uppercase;
font-weight: 300;
color: var(--color-text-pri);
}
}
.sectionTitle {
&.root {
text-decoration: underline;
font-size: 20px;
font-weight: 200;
font-style: italic;
height: 10px;
text-transform: uppercase;
margin-top: 20px;
margin-bottom: 20px;
color: var(--color-text-pri);
}
}
.appName {
&.root {
font-weight: 400;
font-size: 0.8em;
font-style: normal;
color: light-dark(blue, black);
font-family: 'Jost', sans-serif;
margin: 0;
}
}
.appUrl {
&.root {
font-size: 0.8em;
font-weight: 400;
font-style: italic;
text-transform: uppercase;
color: var(--color-text-acc);
margin: 0;
}
}
.linkCategory {
&.root {
font-size: 1.1em;
font-weight: 400;
height: 10px;
color: var(--color-text-acc);
text-transform: uppercase;
margin-top: 20px;
margin-bottom: 20px;
}
}
.linkItem {
&.root {
display: block;
font-size: 1em;
line-height: 2;
color: var(--color-text-pri);
&:hover {
text-decoration: underline;
}
}
}
.settingsLink {
&.root {
font-size: 1.25em;
display: block;
margin-left: 0;
margin-right: 0;
line-height: 2;
justify-self: center;
color: var(--color-text-pri);
border-radius: 1em;
&:hover {
background-color: var(--color-text-acc);
}
}
}
a{

View File

@@ -1,52 +1,50 @@
.keyword{
background-color: transparent;
border: 0;
border-bottom: thin solid var(--color-border);
color: var(--color-text-pri);
font-family: -apple-system, Jost, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif;
font-style: italic;
font-size: 0.8em;
height: 3.5em;
transition: all 0.4s ease;
width: 50%;
padding-left: 1em;
padding-right: 1em;
text-align: center;
.input {
border: 0;
border-bottom: thin solid var(--color-border);
background-color: transparent;
color: var(--color-text-pri);
font-style: italic;
font-size: 0.8em;
height: 3.5em;
transition: all 0.4s ease;
width: 50%;
padding-left: 1em;
padding-right: 1em;
text-align: center;
&:focus {
background-color: var(--color-search-background);
border-color: var(--color-border);
outline: none;
color: var(--color-text-acc);
font-style: normal;
font-weight: 500;
font-size: 1.2em;
letter-spacing: .2em;
width:100%;
border-top-left-radius: .25em;
border-top-right-radius: .25em;
border-bottom-left-radius: .25em;
border-bottom-right-radius: .25em;
}
}
.label{
color: var(--color-border);
display: block;
position: relative;
font-style: normal;
font-weight: 200;
font-size: 1.2em;
letter-spacing: .2em;
opacity: 1;
padding-left: 1em;
padding-right: 1em;
text-align: center;
width: 100%;
top: 28%;
}
}
.keyword:focus{
background-color: var(--color-search-background);
border-color: var(--color-border);
outline: none;
color: var(--color-text-acc);
font-style: normal;
font-weight: 500;
font-size: 1.2em;
letter-spacing: .2em;
width:100%;
border-top-left-radius: .25em;
border-top-right-radius: .25em;
border-bottom-left-radius: .25em;
border-bottom-right-radius: .25em;
}
.keyword::placeholder{
text-align: left;
}
.label{
color: var(--color-border);
display: block;
position: relative;
font-style: normal;
font-weight: 200;
font-size: 1.2em;
letter-spacing: .2em;
opacity: 1;
padding-left: 1em;
padding-right: 1em;
text-align: center;
width: 100%;
top: 28%;
}
.keyword:placeholder-shown + .label{
visibility: hidden;
z-index: 1;

View File

@@ -5,7 +5,8 @@ import navLinkClasses from './NavLink.module.css';
import popoverClasses from './Popover.module.css';
import selectClasses from './Select.module.css';
import stackClasses from './Stack.module.css';
import tabClasses from './Tabs.module.css';
import textClasses from './Text.module.css';
import textInputClasses from './TextInput.module.css';
export { actionIconClasses, buttonClasses, menuClasses, navLinkClasses, popoverClasses, selectClasses, stackClasses, textClasses, textInputClasses }
export { actionIconClasses, buttonClasses, menuClasses, navLinkClasses, popoverClasses, selectClasses, stackClasses, tabClasses, textClasses, textInputClasses }