feature(settingsPage): save links to localstorage
This commit is contained in:
parent
540e79fbff
commit
d36cec3d31
13 changed files with 252 additions and 71 deletions
|
@ -15,9 +15,11 @@
|
|||
"@mui/icons-material": "^5.16.4",
|
||||
"@mui/material": "^5.16.4",
|
||||
"@mui/styled-engine-sc": "6.0.0-alpha.18",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"styled-components": "^6.1.12",
|
||||
"uuid": "^10.0.0",
|
||||
"wouter": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -26,6 +26,9 @@ importers:
|
|||
'@mui/styled-engine-sc':
|
||||
specifier: 6.0.0-alpha.18
|
||||
version: 6.0.0-alpha.18(styled-components@6.1.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
'@types/uuid':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
|
@ -35,6 +38,9 @@ importers:
|
|||
styled-components:
|
||||
specifier: ^6.1.12
|
||||
version: 6.1.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
wouter:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1(react@18.3.1)
|
||||
|
@ -651,6 +657,9 @@ packages:
|
|||
'@types/stylis@4.2.5':
|
||||
resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==}
|
||||
|
||||
'@types/uuid@10.0.0':
|
||||
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
||||
|
||||
'@types/webextension-polyfill@0.10.7':
|
||||
resolution: {integrity: sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==}
|
||||
|
||||
|
@ -1682,6 +1691,10 @@ packages:
|
|||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
hasBin: true
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
@ -2361,6 +2374,8 @@ snapshots:
|
|||
|
||||
'@types/stylis@4.2.5': {}
|
||||
|
||||
'@types/uuid@10.0.0': {}
|
||||
|
||||
'@types/webextension-polyfill@0.10.7': {}
|
||||
|
||||
'@vitejs/plugin-react@4.3.1(vite@5.3.4(@types/node@20.14.11))':
|
||||
|
@ -3385,6 +3400,8 @@ snapshots:
|
|||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
vite-plugin-web-extension@4.1.6(@types/node@20.14.11):
|
||||
|
|
|
@ -11,6 +11,7 @@ import theme from './theme';
|
|||
import { DrawerOpenContextProvider } from './shared/contexts/DrawerOpenContext';
|
||||
import { CurrentPageContextProvider } from './shared/contexts/CurrentPageContext';
|
||||
import { ConfigurableLinksContextProvider } from './shared/contexts/ConfiguarableLinksContext';
|
||||
import { SnackBarValuesContextProvider } from './shared/contexts/SnackBarValuesConext';
|
||||
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
|
@ -19,7 +20,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||
<DrawerOpenContextProvider>
|
||||
<CurrentPageContextProvider>
|
||||
<ConfigurableLinksContextProvider>
|
||||
<SnackBarValuesContextProvider>
|
||||
<App />
|
||||
</SnackBarValuesContextProvider>
|
||||
</ConfigurableLinksContextProvider>
|
||||
</CurrentPageContextProvider>
|
||||
</DrawerOpenContextProvider>
|
||||
|
|
16
src/shared/classes/HomeplageLink.ts
Normal file
16
src/shared/classes/HomeplageLink.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { IHomepageLink } from "../interfaces/IHomepageLink";
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export class HomepageLink implements IHomepageLink {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
hyperlink: string;
|
||||
uuid: string
|
||||
|
||||
constructor() {
|
||||
this.title = ""
|
||||
this.subtitle = ""
|
||||
this.hyperlink = ""
|
||||
this.uuid = uuid.v4()
|
||||
}
|
||||
}
|
41
src/shared/components/AppSnackBar.tsx
Normal file
41
src/shared/components/AppSnackBar.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { useContext } from "react";
|
||||
import { SnackBarValuesContext } from "../contexts/SnackBarValuesConext";
|
||||
import { IconButton, Snackbar, SnackbarCloseReason } from "@mui/material";
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
export default function() {
|
||||
const {snackBarValues, setSnackBarValues} = useContext(SnackBarValuesContext)
|
||||
|
||||
const handleClose = (
|
||||
event: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return
|
||||
}
|
||||
setSnackBarValues({open: false, message: ""})
|
||||
}
|
||||
|
||||
const action = (
|
||||
<>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon fontSize="small"/>
|
||||
</IconButton>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={snackBarValues.open}
|
||||
autoHideDuration={5000}
|
||||
message={snackBarValues.message}
|
||||
onClose={handleClose}
|
||||
action={action}
|
||||
/>
|
||||
)
|
||||
}
|
32
src/shared/components/LinkList.tsx
Normal file
32
src/shared/components/LinkList.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Sites from '@Data/sites.json'
|
||||
import { Grid } from "@mui/material";
|
||||
import LinkCard from "./linkcard";
|
||||
import { useContext } from 'react';
|
||||
import { ConfigurableLinksContext } from '../contexts/ConfiguarableLinksContext';
|
||||
|
||||
export default function LinkList() {
|
||||
const {configurableLinks, setConfigurableLinks} = useContext(ConfigurableLinksContext)
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{Sites.sites.map(site => (
|
||||
<Grid item key={site.title}>
|
||||
<LinkCard
|
||||
title={site.title}
|
||||
subtitle={site.subtitle}
|
||||
hyperlink={site.hyperlink}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
{configurableLinks.map(link => (
|
||||
<Grid item key={link.title}>
|
||||
<LinkCard
|
||||
title={link.title}
|
||||
subtitle={link.subtitle}
|
||||
hyperlink={link.hyperlink}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
|
@ -1,69 +1,120 @@
|
|||
import { Button, Grid, styled, TextField, Typography } from "@mui/material";
|
||||
import { useContext, useState } from "react";
|
||||
import { HomepageLink } from "../interfaces/HomepageLink.interface";
|
||||
import { ConfigurableLinksContext } from "../contexts/ConfiguarableLinksContext";
|
||||
|
||||
const FormGrid = styled(Grid)(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}))
|
||||
|
||||
interface FormRowProps {
|
||||
index: number
|
||||
link: HomepageLink
|
||||
}
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { IHomepageLink } from "../interfaces/IHomepageLink";
|
||||
import { HomepageLink } from "../classes/HomeplageLink";
|
||||
import { SnackBarValuesContext } from "../contexts/SnackBarValuesConext";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const {configurableLinks, setConfigurableLinks} = useContext(ConfigurableLinksContext)
|
||||
const [configurableLinks, setConfigurableLinks] = useState<Array<HomepageLink>>(
|
||||
JSON.parse(localStorage.getItem("configurableLinks") as string) as Array<HomepageLink> ??
|
||||
new Array<HomepageLink>
|
||||
)
|
||||
const {snackBarValues, setSnackBarValues} = useContext(SnackBarValuesContext)
|
||||
|
||||
|
||||
const handleChange = (index: number, event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
let newconfigurableLinks = [...configurableLinks]
|
||||
newconfigurableLinks[index][event.target.name as keyof HomepageLink] = event.target.value
|
||||
newconfigurableLinks[index][event.target.name as keyof IHomepageLink] = event.target.value
|
||||
setConfigurableLinks(newconfigurableLinks)
|
||||
}
|
||||
|
||||
const addFields = () => {
|
||||
setConfigurableLinks([...configurableLinks, {} as HomepageLink])
|
||||
const handleBlur = () => {
|
||||
if (LinkNotEmpty(configurableLinks[configurableLinks.length -1])) {
|
||||
addFields()
|
||||
}
|
||||
if (configurableLinks.length > 1) {
|
||||
if (LinkEmpty(configurableLinks[configurableLinks.length -2])) {
|
||||
deleteFieldsAt(configurableLinks.length -2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function LinkNotEmpty(link: IHomepageLink): Boolean {
|
||||
return link.title != "" &&
|
||||
link.subtitle != "" &&
|
||||
link.hyperlink != ""
|
||||
}
|
||||
|
||||
function LinkEmpty(link: IHomepageLink): Boolean {
|
||||
return link.title == "" &&
|
||||
link.subtitle == "" &&
|
||||
link.hyperlink == ""
|
||||
}
|
||||
|
||||
const addFields = () => {
|
||||
setConfigurableLinks([...configurableLinks, new HomepageLink()])
|
||||
}
|
||||
|
||||
const deleteFieldsAt = (index: number) => {
|
||||
setConfigurableLinks(configurableLinks.filter((link) => {return link != configurableLinks[index]}))
|
||||
}
|
||||
|
||||
const saveToLocalStorage = () => {
|
||||
localStorage.setItem("configurableLinks", JSON.stringify(configurableLinks.filter((link) => {return link != configurableLinks[configurableLinks.length -1]})))
|
||||
setSnackBarValues({open: true, message: "Saved Links"})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
addFields()
|
||||
return () => {
|
||||
alert("Will close settings")
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} sx={{mt: 4, mb: 4}}>
|
||||
<Grid container spacing={1}>
|
||||
{configurableLinks.map((link, index) =>(
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<Grid container item spacing={3} key={link.uuid}>
|
||||
<Grid item xs={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
required
|
||||
label='Title'
|
||||
name='title'
|
||||
value={link.title}
|
||||
key={`title${index}`}
|
||||
onChange={e => handleChange(index, e)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
required
|
||||
label='Subtitle'
|
||||
name='subtitle'
|
||||
value={link.subtitle}
|
||||
key={`subtitle${index}`}
|
||||
onChange={e => handleChange(index, e)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
required
|
||||
label='Hyperlink'
|
||||
name='hyperlink'
|
||||
value={link.hyperlink}
|
||||
key={`hyperlink${index}`}
|
||||
onChange={e => handleChange(index, e)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
{(configurableLinks.length > 1 && index != configurableLinks.length -1)? (
|
||||
<Grid item xs={2} display={"flex"} alignItems={"center"} justifyContent={"center"}>
|
||||
<Button
|
||||
onClick={() => deleteFieldsAt(index)}
|
||||
variant="contained"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Grid>
|
||||
):null}
|
||||
</Grid>
|
||||
))}
|
||||
<Grid>
|
||||
<Button onClick={addFields}>Add field</Button>
|
||||
<Button onClick={() => saveToLocalStorage()}>Save</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { createContext, Dispatch, SetStateAction, useState } from "react"
|
||||
import { HomepageLink } from "../interfaces/HomepageLink.interface"
|
||||
import { IHomepageLink } from "../interfaces/IHomepageLink"
|
||||
import { ContainerProps } from "@mui/material"
|
||||
|
||||
type ConfigurableLinksContextType = {
|
||||
configurableLinks: Array<HomepageLink>,
|
||||
setConfigurableLinks: Dispatch<SetStateAction<Array<HomepageLink>>>
|
||||
configurableLinks: Array<IHomepageLink>,
|
||||
setConfigurableLinks: Dispatch<SetStateAction<Array<IHomepageLink>>>
|
||||
}
|
||||
|
||||
const ConfigurableLinksContext = createContext<ConfigurableLinksContextType>({
|
||||
configurableLinks: new Array<HomepageLink>,
|
||||
configurableLinks: new Array<IHomepageLink>,
|
||||
setConfigurableLinks: () => {}
|
||||
})
|
||||
|
||||
const ConfigurableLinksContextProvider = (props: ContainerProps) => {
|
||||
const [configurableLinks, setConfigurableLinks] = useState(new Array<HomepageLink>)
|
||||
const [configurableLinks, setConfigurableLinks] = useState(new Array<IHomepageLink>)
|
||||
|
||||
return (
|
||||
<ConfigurableLinksContext.Provider value={{configurableLinks, setConfigurableLinks}}>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { ContainerProps } from "@mui/material";
|
||||
import { createContext, Dispatch, SetStateAction, useState } from "react";
|
||||
|
||||
|
|
25
src/shared/contexts/SnackBarValuesConext.tsx
Normal file
25
src/shared/contexts/SnackBarValuesConext.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { createContext, Dispatch, SetStateAction, useState } from "react"
|
||||
import { ISnackBarValues } from "../interfaces/ISnackBarValues"
|
||||
import { ContainerProps } from "@mui/material"
|
||||
|
||||
type SnackBarValuesContextType = {
|
||||
snackBarValues: ISnackBarValues,
|
||||
setSnackBarValues: Dispatch<SetStateAction<ISnackBarValues>>
|
||||
}
|
||||
|
||||
const SnackBarValuesContext = createContext<SnackBarValuesContextType>({
|
||||
snackBarValues: {open: false, message: ""} as ISnackBarValues,
|
||||
setSnackBarValues: () => {}
|
||||
})
|
||||
|
||||
const SnackBarValuesContextProvider = (props: ContainerProps) => {
|
||||
const [snackBarValues, setSnackBarValues] = useState<ISnackBarValues>({open: false, message: ""})
|
||||
|
||||
return (
|
||||
<SnackBarValuesContext.Provider value={{snackBarValues, setSnackBarValues}}>
|
||||
{props.children}
|
||||
</SnackBarValuesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export {SnackBarValuesContext, SnackBarValuesContextProvider}
|
|
@ -1,4 +1,4 @@
|
|||
export interface HomepageLink {
|
||||
export interface IHomepageLink {
|
||||
title: string
|
||||
subtitle: string,
|
||||
hyperlink: string
|
4
src/shared/interfaces/ISnackBarValues.ts
Normal file
4
src/shared/interfaces/ISnackBarValues.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface ISnackBarValues {
|
||||
open: boolean
|
||||
message: string
|
||||
}
|
|
@ -3,10 +3,11 @@ import { Box, Button, debounce, FormLabel, Grid, OutlinedInput, styled, TextFiel
|
|||
import React, { useContext, useState } from 'react'
|
||||
import MenuBar from '../shared/components/MenuBar'
|
||||
import LinkCard from '../shared/components/linkcard'
|
||||
import { Link, Route } from 'wouter'
|
||||
import MenuDrawer from '../shared/components/menudrawer'
|
||||
import { CurrentPageContext } from '../shared/contexts/CurrentPageContext'
|
||||
import SettingsPage from '../shared/components/settingsPage'
|
||||
import LinkList from '../shared/components/LinkList'
|
||||
import AppSnackBar from '../shared/components/AppSnackBar'
|
||||
|
||||
const useLocalStorage = (storageKey: string, fallbackState: any) => {
|
||||
const [value, setValue] = React.useState(
|
||||
|
@ -21,7 +22,6 @@ const useLocalStorage = (storageKey: string, fallbackState: any) => {
|
|||
|
||||
function App() {
|
||||
const {currentPage, setCurrentPage} = useContext(CurrentPageContext)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -29,22 +29,13 @@ function App() {
|
|||
<MenuDrawer />
|
||||
<Box sx={{m: 4}} maxWidth="lg">
|
||||
{currentPage === 'home' ? (
|
||||
<Grid container spacing={2}>
|
||||
{Sites.sites.map(site => (
|
||||
<Grid item key={site.title}>
|
||||
<LinkCard
|
||||
title={site.title}
|
||||
subtitle={site.subtitle}
|
||||
hyperlink={site.hyperlink}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<LinkList />
|
||||
): null}
|
||||
{currentPage === 'edit' ? (
|
||||
<SettingsPage />
|
||||
): null}
|
||||
</Box>
|
||||
<AppSnackBar />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue