PDF Markings #114
62
package-lock.json
generated
@ -10,6 +10,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "11.11.4",
|
"@emotion/react": "11.11.4",
|
||||||
"@emotion/styled": "11.11.0",
|
"@emotion/styled": "11.11.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@mui/icons-material": "5.15.11",
|
"@mui/icons-material": "5.15.11",
|
||||||
"@mui/lab": "5.0.0-alpha.166",
|
"@mui/lab": "5.0.0-alpha.166",
|
||||||
"@mui/material": "5.15.11",
|
"@mui/material": "5.15.11",
|
||||||
@ -1095,6 +1099,64 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
||||||
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
|
||||||
|
"license": "(CC-BY-4.0 AND MIT)",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
||||||
|
"license": "(CC-BY-4.0 AND MIT)",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/react-fontawesome": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||||
|
"react": ">=16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||||
|
@ -16,6 +16,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "11.11.4",
|
"@emotion/react": "11.11.4",
|
||||||
"@emotion/styled": "11.11.0",
|
"@emotion/styled": "11.11.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@mui/icons-material": "5.15.11",
|
"@mui/icons-material": "5.15.11",
|
||||||
"@mui/lab": "5.0.0-alpha.166",
|
"@mui/lab": "5.0.0-alpha.166",
|
||||||
"@mui/material": "5.15.11",
|
"@mui/material": "5.15.11",
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/logo.png
34
public/logo.svg
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 734.95 255.06">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #47b17d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #4c82a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #7d54a3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M335.64,100.98c0-4.9,.94-9.5,2.81-13.79,1.87-4.29,4.42-8.05,7.64-11.27,3.22-3.22,6.98-5.77,11.27-7.64,4.29-1.87,8.89-2.81,13.79-2.81h54.34v23.7h-54.34c-1.65,0-3.19,.3-4.62,.91-1.43,.61-2.68,1.45-3.76,2.52s-1.91,2.33-2.52,3.76c-.61,1.43-.91,2.97-.91,4.62s.3,3.21,.91,4.67c.61,1.46,1.45,2.73,2.52,3.8,1.07,1.07,2.33,1.91,3.76,2.52,1.43,.61,2.97,.91,4.62,.91h23.7c4.9,0,9.51,.92,13.83,2.77,4.32,1.85,8.09,4.38,11.31,7.6s5.75,6.99,7.6,11.31c1.84,4.32,2.77,8.93,2.77,13.83s-.92,9.5-2.77,13.79c-1.85,4.29-4.38,8.05-7.6,11.27s-6.99,5.77-11.31,7.64c-4.32,1.87-8.93,2.81-13.83,2.81h-52.61v-23.7h52.61c1.65,0,3.19-.3,4.62-.91,1.43-.61,2.68-1.45,3.76-2.52s1.91-2.33,2.52-3.76c.61-1.43,.91-2.97,.91-4.62s-.3-3.19-.91-4.62c-.61-1.43-1.45-2.68-2.52-3.76-1.07-1.07-2.33-1.91-3.76-2.52-1.43-.6-2.97-.91-4.62-.91h-23.7c-4.9,0-9.5-.94-13.79-2.81-4.29-1.87-8.05-4.42-11.27-7.64-3.22-3.22-5.77-6.99-7.64-11.31-1.87-4.32-2.81-8.93-2.81-13.83Z"/>
|
||||||
|
<path class="cls-3" d="M469.59,183.9h-23.7V65.47h23.7v118.43Z"/>
|
||||||
|
<path class="cls-3" d="M585.8,171.92c-5.51,4.68-11.64,8.27-18.42,10.78-6.77,2.5-13.82,3.76-21.14,3.76-5.62,0-11.03-.73-16.23-2.19-5.2-1.46-10.06-3.52-14.58-6.19-4.52-2.67-8.65-5.86-12.39-9.58-3.75-3.72-6.94-7.85-9.58-12.39s-4.7-9.43-6.15-14.66c-1.46-5.23-2.19-10.65-2.19-16.27s.73-11.01,2.19-16.19c1.46-5.17,3.51-10.03,6.15-14.58,2.64-4.54,5.83-8.67,9.58-12.39,3.74-3.72,7.87-6.9,12.39-9.54,4.51-2.64,9.37-4.69,14.58-6.15s10.61-2.19,16.23-2.19c7.32,0,14.37,1.25,21.14,3.76,6.77,2.51,12.91,6.1,18.42,10.78l-12.39,20.65c-3.58-3.63-7.71-6.48-12.39-8.55-4.68-2.06-9.61-3.1-14.78-3.1s-10.03,.99-14.58,2.97c-4.54,1.98-8.52,4.67-11.93,8.05-3.41,3.39-6.11,7.35-8.09,11.89-1.98,4.54-2.97,9.4-2.97,14.58s.99,10.13,2.97,14.7c1.98,4.57,4.68,8.56,8.09,11.98,3.41,3.41,7.39,6.11,11.93,8.09,4.54,1.98,9.4,2.97,14.58,2.97,2.97,0,5.86-.36,8.67-1.07,2.81-.71,5.48-1.71,8.01-2.97v-33.7h22.88v46.75Z"/>
|
||||||
|
<path class="cls-3" d="M627.75,183.9h-23.7V65.47h23.7v118.43Z"/>
|
||||||
|
<path class="cls-3" d="M699.44,183.9h-23.62V89.17h-35.6v-23.7h94.73v23.7h-35.51v94.73Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M181.53,115.06h0c-9.4-36.67-56.77-24.79-121.09-12.57C-3.54,114.64-25.35,19.85,37.72,3.62,46.91,1.26,56.55,0,66.47,0c63.55,0,115.06,51.51,115.06,115.06Z"/>
|
||||||
|
<path class="cls-1" d="M100,140h0c9.4,36.67,56.77,24.79,121.09,12.57,63.98-12.16,85.79,82.64,22.72,98.86-9.19,2.36-18.83,3.62-28.76,3.62-63.55,0-115.06-51.51-115.06-115.06Z"/>
|
||||||
|
<circle class="cls-3" cx="140.77" cy="127.53" r="24.88"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
42
src/App.css
@ -1,42 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
65
src/App.scss
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
@import './styles/colors.scss';
|
||||||
|
@import './styles/sizes.scss';
|
||||||
|
@import './styles/typography.scss';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
// Import colors once as variables
|
||||||
|
--primary-main: #{$primary-main};
|
||||||
|
--primary-light: #{$primary-light};
|
||||||
|
--primary-dark: #{$primary-dark};
|
||||||
|
|
||||||
|
--secondary-main: #{$secondary-main};
|
||||||
|
|
||||||
|
--box-shadow-color: #{$box-shadow-color};
|
||||||
|
--border-color: #{$border-color};
|
||||||
|
|
||||||
|
--body-background-color: #{$body-background-color};
|
||||||
|
--overlay-background-color: #{$overlay-background-color};
|
||||||
|
|
||||||
|
--text-color: #{$text-color};
|
||||||
|
--input-text-color: #{$input-text-color};
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: $text-color;
|
||||||
|
font-family: $font-familiy;
|
||||||
|
letter-spacing: $letter-spacing;
|
||||||
|
font-size: $body-font-size;
|
||||||
|
font-weight: $body-font-weight;
|
||||||
|
line-height: $body-line-height;
|
||||||
|
|
||||||
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: $primary-main;
|
||||||
|
text-decoration: none;
|
||||||
|
text-decoration-color: inherit;
|
||||||
|
transition: ease 0.4s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-light;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: inherit;
|
||||||
|
}
|
||||||
|
}
|
43
src/App.tsx
@ -7,10 +7,12 @@ import {
|
|||||||
appPrivateRoutes,
|
appPrivateRoutes,
|
||||||
appPublicRoutes,
|
appPublicRoutes,
|
||||||
privateRoutes,
|
privateRoutes,
|
||||||
publicRoutes
|
publicRoutes,
|
||||||
|
recursiveRouteRenderer
|
||||||
} from './routes'
|
} from './routes'
|
||||||
import { State } from './store/rootReducer'
|
import { State } from './store/rootReducer'
|
||||||
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
||||||
|
import './App.scss'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const authState = useSelector((state: State) => state.auth)
|
const authState = useSelector((state: State) => state.auth)
|
||||||
@ -48,39 +50,18 @@ const App = () => {
|
|||||||
return `${appPublicRoutes.login}?callbackPath=${callbackPathEncoded}`
|
return `${appPublicRoutes.login}?callbackPath=${callbackPathEncoded}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true
|
||||||
|
const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => {
|
||||||
|
return !authState.loggedIn || !r.hiddenWhenLoggedIn
|
||||||
|
})
|
||||||
|
|
||||||
|
const privateRouteList = recursiveRouteRenderer(privateRoutes)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
{authState?.loggedIn &&
|
{authState?.loggedIn && privateRouteList}
|
||||||
privateRoutes.map((route, index) => (
|
{publicRoutesList}
|
||||||
<Route
|
|
||||||
key={route.path + index}
|
|
||||||
path={route.path}
|
|
||||||
element={route.element}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{publicRoutes.map((route, index) => {
|
|
||||||
if (authState?.loggedIn) {
|
|
||||||
if (!route.hiddenWhenLoggedIn) {
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
key={route.path + index}
|
|
||||||
path={route.path}
|
|
||||||
element={route.element}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
key={route.path + index}
|
|
||||||
path={route.path}
|
|
||||||
element={route.element}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to={handleRootRedirect()} />} />
|
<Route path="*" element={<Navigate to={handleRootRedirect()} />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
BIN
src/assets/fonts/roboto-bold.woff
Normal file
BIN
src/assets/fonts/roboto-bold.woff2
Normal file
BIN
src/assets/fonts/roboto-light.woff
Normal file
BIN
src/assets/fonts/roboto-light.woff2
Normal file
BIN
src/assets/fonts/roboto-medium.woff
Normal file
BIN
src/assets/fonts/roboto-medium.woff2
Normal file
BIN
src/assets/fonts/roboto-regular.woff
Normal file
BIN
src/assets/fonts/roboto-regular.woff2
Normal file
6
src/assets/images/bg_l.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 319.87">
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<path d="M113.31,3.31C73.23,10.93,35.28,18.5,0,24.8V307.11c30.14,8.31,61.89,12.76,94.68,12.76,30.67,0,60.43-3.88,88.82-11.19C378.31,258.56,310.93-34.23,113.31,3.31Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
6
src/assets/images/bg_r.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 319.87">
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<path d="M205.32,0c-30.67,0-60.43,3.88-88.82,11.19C-78.31,61.31-10.93,354.1,186.69,316.56c40.08-7.61,78.02-15.18,113.31-21.49V12.76C269.86,4.45,238.11,0,205.32,0Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 362 B After Width: | Height: | Size: 362 B |
BIN
src/assets/images/nostr.gif
Normal file
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
BIN
src/assets/images/placeholder.png
Normal file
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -1,50 +0,0 @@
|
|||||||
$primary-main: var(--mui-palette-primary-main);
|
|
||||||
$primary-light: var(--mui-palette-primary-light);
|
|
||||||
$primary-dark: var(--mui-palette-primary-dark);
|
|
||||||
|
|
||||||
$box-shadow-color: rgba(0, 0, 0, 0.1);
|
|
||||||
$border-color: #27323c;
|
|
||||||
|
|
||||||
$page-background-color: #f4f4fb;
|
|
||||||
$modal-input-background: #f4f4fb;
|
|
||||||
$background-color: #fff;
|
|
||||||
|
|
||||||
$text-color: #3e3e3e;
|
|
||||||
$input-text-color: #717171;
|
|
||||||
$info-text-color: #4169e1;
|
|
||||||
|
|
||||||
$icon-color: #c1c1c1;
|
|
||||||
$zap-icon-color: #ffd700;
|
|
||||||
|
|
||||||
$btn-background-color: $primary-main;
|
|
||||||
$btn-color: #fff;
|
|
||||||
|
|
||||||
$progress-box-background-color: $primary-main;
|
|
||||||
$progress-box-inner-background-color: $primary-dark;
|
|
||||||
$rank-progress-background-color: #445c76;
|
|
||||||
|
|
||||||
$correct-answer-background-color: $primary-light;
|
|
||||||
$incorrect-answer-background-color: #e84d67;
|
|
||||||
$incorrect-answer-label-color: #fff;
|
|
||||||
|
|
||||||
$checkbox-border-color: #ccc;
|
|
||||||
$checkbox-hover-border-color: #888;
|
|
||||||
$checkbox-checked-background-color: $primary-main;
|
|
||||||
$checkbox-checked-color: #fff;
|
|
||||||
|
|
||||||
$comment-notice-background-color: #0d6efd;
|
|
||||||
$comment-notice-color: #fff;
|
|
||||||
$comment-divider-color: #eee;
|
|
||||||
|
|
||||||
$activated-topic-background-color: var(--mui-palette-info-dark);
|
|
||||||
|
|
||||||
$error-msg-color: red;
|
|
||||||
$suggestion-box-heading-color: #dfe0e0;
|
|
||||||
|
|
||||||
$callOut-success-background: $primary-light;
|
|
||||||
$callOut-success-color: $primary-dark;
|
|
||||||
|
|
||||||
$review-feedback-correct: #178b13;
|
|
||||||
$review-feedback-incorrect: #d82222;
|
|
||||||
$review-feedback-neutral: #f39220;
|
|
||||||
$review-feedback-selected-color: #fff;
|
|
@ -34,6 +34,8 @@ import {
|
|||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { setUserRobotImage } from '../../store/userRobotImage/action'
|
import { setUserRobotImage } from '../../store/userRobotImage/action'
|
||||||
|
import { Container } from '../Container'
|
||||||
|
import { ButtonIcon } from '../ButtonIcon'
|
||||||
|
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
@ -117,112 +119,124 @@ export const AppBar = () => {
|
|||||||
const isAuthenticated = authState?.loggedIn === true
|
const isAuthenticated = authState?.loggedIn === true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBarMui position="fixed" className={styles.AppBar}>
|
<AppBarMui
|
||||||
<Toolbar className={styles.toolbar}>
|
position="fixed"
|
||||||
<Box className={styles.logoWrapper}>
|
className={styles.AppBar}
|
||||||
<img src="/logo.png" alt="Logo" onClick={() => navigate('/')} />
|
sx={{
|
||||||
</Box>
|
boxShadow: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<Toolbar className={styles.toolbar} disableGutters={true}>
|
||||||
|
<Box className={styles.logoWrapper}>
|
||||||
|
<img src="/logo.svg" alt="Logo" onClick={() => navigate('/')} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box className={styles.rightSideBox}>
|
<Box className={styles.rightSideBox}>
|
||||||
{!isAuthenticated && (
|
{!isAuthenticated && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
startIcon={<ButtonIcon />}
|
||||||
navigate(appPublicRoutes.login)
|
onClick={() => {
|
||||||
}}
|
navigate(appPublicRoutes.nostr)
|
||||||
variant="contained"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isAuthenticated && (
|
|
||||||
<>
|
|
||||||
<Username
|
|
||||||
username={username}
|
|
||||||
avatarContent={userAvatar}
|
|
||||||
handleClick={handleOpenUserMenu}
|
|
||||||
/>
|
|
||||||
<Menu
|
|
||||||
id="menu-appbar"
|
|
||||||
anchorEl={anchorElUser}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
}}
|
||||||
keepMounted
|
variant="contained"
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
open={!!anchorElUser}
|
|
||||||
onClose={handleCloseUserMenu}
|
|
||||||
>
|
>
|
||||||
<MenuItem
|
LOGIN
|
||||||
sx={{
|
</Button>
|
||||||
justifyContent: 'center',
|
)}
|
||||||
display: { md: 'none' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6">{username}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={handleProfile}
|
|
||||||
sx={{
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setAnchorElUser(null)
|
|
||||||
|
|
||||||
navigate(appPrivateRoutes.settings)
|
{isAuthenticated && (
|
||||||
|
<>
|
||||||
|
<Username
|
||||||
|
username={username}
|
||||||
|
avatarContent={userAvatar}
|
||||||
|
handleClick={handleOpenUserMenu}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
id="menu-appbar"
|
||||||
|
anchorEl={anchorElUser}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
}}
|
}}
|
||||||
sx={{
|
keepMounted
|
||||||
justifyContent: 'center'
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
open={!!anchorElUser}
|
||||||
Settings
|
onClose={handleCloseUserMenu}
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setAnchorElUser(null)
|
|
||||||
|
|
||||||
navigate(appPublicRoutes.verify)
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Verify
|
|
||||||
</MenuItem>
|
|
||||||
<Link
|
|
||||||
to={appPublicRoutes.source}
|
|
||||||
target="_blank"
|
|
||||||
style={{ color: 'inherit', textDecoration: 'inherit' }}
|
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
display: { md: 'none' },
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '14px',
|
||||||
|
color: 'var(--text-color)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">{username}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleProfile}
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Source
|
Profile
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Link>
|
<MenuItem
|
||||||
<MenuItem
|
onClick={() => {
|
||||||
onClick={handleLogout}
|
setAnchorElUser(null)
|
||||||
sx={{
|
|
||||||
justifyContent: 'center'
|
navigate(appPrivateRoutes.settings)
|
||||||
}}
|
}}
|
||||||
>
|
sx={{
|
||||||
Logout
|
justifyContent: 'center'
|
||||||
</MenuItem>
|
}}
|
||||||
</Menu>
|
>
|
||||||
</>
|
Settings
|
||||||
)}
|
</MenuItem>
|
||||||
</Box>
|
<MenuItem
|
||||||
</Toolbar>
|
onClick={() => {
|
||||||
|
setAnchorElUser(null)
|
||||||
|
|
||||||
|
navigate(appPublicRoutes.verify)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</MenuItem>
|
||||||
|
<Link
|
||||||
|
to={appPublicRoutes.source}
|
||||||
|
target="_blank"
|
||||||
|
style={{ color: 'inherit', textDecoration: 'inherit' }}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
</MenuItem>
|
||||||
|
</Link>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleLogout}
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</Container>
|
||||||
</AppBarMui>
|
</AppBarMui>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
@import '../../styles/sizes.scss';
|
||||||
|
|
||||||
.AppBar {
|
.AppBar {
|
||||||
background-color: $background-color !important;
|
background-color: $overlay-background-color !important;
|
||||||
z-index: 1400 !important;
|
height: $header-height;
|
||||||
height: 60px;
|
|
||||||
flex-direction: row !important;
|
flex-direction: row !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
border-bottom: solid 1px rgba(0, 0, 0, 0.075);
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
14
src/components/ButtonIcon/index.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import styles from './style.module.scss'
|
||||||
|
import placeholder from '../../assets/images/placeholder.png'
|
||||||
|
|
||||||
|
interface ButtonIconProps {
|
||||||
|
src?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonIcon = ({
|
||||||
|
src = placeholder,
|
||||||
|
alt = ''
|
||||||
|
}: ButtonIconProps) => {
|
||||||
|
return <img src={src} alt={alt} className={styles.icon}></img>
|
||||||
|
}
|
5
src/components/ButtonIcon/style.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.icon {
|
||||||
|
border-radius: 100px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
24
src/components/Container/index.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { CSSProperties, PropsWithChildren } from 'react'
|
||||||
|
import defaultStyle from './style.module.scss'
|
||||||
|
|
||||||
|
interface ContainerProps {
|
||||||
|
style?: CSSProperties
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Container = ({
|
||||||
|
style = {},
|
||||||
|
className = '',
|
||||||
|
children
|
||||||
|
}: PropsWithChildren<ContainerProps>) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
className={`${defaultStyle.container} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
8
src/components/Container/style.module.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@import '../../styles/sizes.scss';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
padding-inline: $default-container-padding-inline;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
10
src/components/FontAwesomeIconStack/index.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
import styles from './styles.module.scss'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Component overlays FontAwesomeIcon icons on top of each other
|
||||||
|
*/
|
||||||
|
export const FontAwesomeIconStack = ({ children }: PropsWithChildren) => {
|
||||||
|
return <div className={styles.iconStackContainer}>{children}</div>
|
||||||
|
}
|
7
src/components/FontAwesomeIconStack/styles.module.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.iconStackContainer {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
128
src/components/Footer/Footer.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { Box, Button, Link as LinkMui } from '@mui/material'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { Container } from '../Container'
|
||||||
|
import nostrImage from '../../assets/images/nostr.gif'
|
||||||
|
import { appPublicRoutes } from '../../routes'
|
||||||
|
|
||||||
|
export const Footer = () => (
|
||||||
|
<footer className={`${styles.borderTop} ${styles.footer}`}>
|
||||||
|
<Container
|
||||||
|
style={{
|
||||||
|
paddingBlock: '50px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display={'grid'}
|
||||||
|
sx={{
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: '1fr',
|
||||||
|
md: '0.5fr 2fr 0.5fr'
|
||||||
|
},
|
||||||
|
alignItems: {
|
||||||
|
xs: 'center',
|
||||||
|
md: 'start'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
gap={'50px'}
|
||||||
|
>
|
||||||
|
<LinkMui
|
||||||
|
sx={{
|
||||||
|
justifySelf: {
|
||||||
|
xs: 'center',
|
||||||
|
md: 'start'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
component={Link}
|
||||||
|
to={'/'}
|
||||||
|
className={styles.logo}
|
||||||
|
>
|
||||||
|
<img src="/logo.svg" alt="Logo" />
|
||||||
|
</LinkMui>
|
||||||
|
<Box
|
||||||
|
display={'grid'}
|
||||||
|
sx={{
|
||||||
|
gap: '15px',
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: '1fr',
|
||||||
|
sm: 'repeat(2, 1fr)',
|
||||||
|
xl: 'repeat(3, 1fr)'
|
||||||
|
},
|
||||||
|
borderBlock: {
|
||||||
|
xs: 'solid 1px rgba(0, 0, 0, 0.1)',
|
||||||
|
md: 'unset'
|
||||||
|
},
|
||||||
|
paddingY: {
|
||||||
|
xs: '10px',
|
||||||
|
md: 'unset'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
component={'nav'}
|
||||||
|
className={styles.nav}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
component={Link}
|
||||||
|
to={'/'}
|
||||||
|
variant={'text'}
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
component={LinkMui}
|
||||||
|
href={appPublicRoutes.docs}
|
||||||
|
target="_blank"
|
||||||
|
variant={'text'}
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
component={LinkMui}
|
||||||
|
href={appPublicRoutes.source}
|
||||||
|
target="_blank"
|
||||||
|
variant={'text'}
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className={styles.links}
|
||||||
|
sx={{
|
||||||
|
justifySelf: {
|
||||||
|
xs: 'center',
|
||||||
|
md: 'end'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
component={LinkMui}
|
||||||
|
href="https://snort.social/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv"
|
||||||
|
target="_blank"
|
||||||
|
sx={{
|
||||||
|
minWidth: '45px',
|
||||||
|
padding: '10px'
|
||||||
|
}}
|
||||||
|
variant={'contained'}
|
||||||
|
>
|
||||||
|
<img src={nostrImage} width="25" alt="nostr logo" height="25" />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
<div className={`${styles.borderTop} ${styles.credits}`}>
|
||||||
|
Built by
|
||||||
|
<a href="https://nostrdev.com/" target="_blank">
|
||||||
|
Nostr Dev
|
||||||
|
</a>{' '}
|
||||||
|
2024.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
49
src/components/Footer/style.module.scss
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.borderTop {
|
||||||
|
border-top: solid 1px rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
> a + a {
|
||||||
|
margin-left: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
a {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.credits {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
28
src/components/Landing/CardComponent/CardComponent.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
|
interface CardComponentProps {
|
||||||
|
icon: ReactElement
|
||||||
|
title: ReactElement
|
||||||
|
description: ReactElement
|
||||||
|
actions?: ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardComponent = ({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions
|
||||||
|
}: CardComponentProps) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.card}>
|
||||||
|
<h3 className={styles.title}>
|
||||||
|
<div className={styles.icon}>{icon}</div>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className={styles.description}>{description}</p>
|
||||||
|
{actions ? <div className={styles.actions}>{actions}</div> : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
64
src/components/Landing/CardComponent/style.module.scss
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
@import '../../../styles/colors.scss';
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 25px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
background: $overlay-background-color;
|
||||||
|
|
||||||
|
transition: ease 0.2s;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background: $primary-main;
|
||||||
|
|
||||||
|
.icon,
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
transition:
|
||||||
|
color 0s,
|
||||||
|
background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: $primary-main;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
grid-gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
@ -11,7 +11,7 @@ export const LoadingSpinner = (props: Props) => {
|
|||||||
<div className={styles.loadingSpinnerOverlay}>
|
<div className={styles.loadingSpinnerOverlay}>
|
||||||
<div className={styles.loadingSpinnerContainer}>
|
<div className={styles.loadingSpinnerContainer}>
|
||||||
<div className={styles.loadingSpinner}></div>
|
<div className={styles.loadingSpinner}></div>
|
||||||
{desc && <span>{desc}</span>}
|
{desc && <span className={styles.loadingSpinnerDesc}>{desc}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.loadingSpinnerOverlay {
|
.loadingSpinnerOverlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -18,15 +20,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loadingSpinner {
|
.loadingSpinner {
|
||||||
border: 4px solid #f3f3f3;
|
background: url('/favicon.png') no-repeat center / cover;
|
||||||
border-top: 4px solid #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadingSpinnerDesc {
|
||||||
|
color: white;
|
||||||
|
margin-top: 13px;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
42
src/components/UserAvatar/index.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { getProfileRoute } from '../../routes'
|
||||||
|
|
||||||
|
import styles from './styles.module.scss'
|
||||||
|
import React from 'react'
|
||||||
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
|
|
||||||
|
interface UserAvatarProps {
|
||||||
|
name: string
|
||||||
|
pubkey: string
|
||||||
|
image?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component will be used for the displaying username and profile picture.
|
||||||
|
* Clicking will navigate to the user's profile.
|
||||||
|
*/
|
||||||
|
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
navigate(getProfileRoute(pubkey))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<AvatarIconButton
|
||||||
|
src={image}
|
||||||
|
hexKey={pubkey}
|
||||||
|
aria-label={`account of user ${name}`}
|
||||||
|
color="inherit"
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
{name ? (
|
||||||
|
<label onClick={handleClick} className={styles.username}>
|
||||||
|
{name}
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
13
src/components/UserAvatar/styles.module.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
32
src/components/UserAvatarIconButton/index.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { IconButton, IconButtonProps } from '@mui/material'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { getRoboHashPicture } from '../../utils'
|
||||||
|
|
||||||
|
interface AvatarIconButtonProps extends IconButtonProps {
|
||||||
|
src: string | undefined
|
||||||
|
hexKey: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component displays profile image inside IconButton
|
||||||
|
* @param {string | undefined} props.src - image source or robohash picture
|
||||||
|
* @param {string | undefined} props.hexKey - robohash and affects border
|
||||||
|
* @param {IconButtonProps} props - component extends mui's IconButton
|
||||||
|
*/
|
||||||
|
export const AvatarIconButton = (props: AvatarIconButtonProps) => {
|
||||||
|
const { src, hexKey, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton {...rest}>
|
||||||
|
<img
|
||||||
|
src={src || getRoboHashPicture(hexKey)}
|
||||||
|
alt="user image"
|
||||||
|
className={`${styles.icon}`}
|
||||||
|
style={{
|
||||||
|
borderStyle: hexKey ? 'solid' : 'none',
|
||||||
|
borderColor: `#${hexKey?.substring(0, 6)}`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
7
src/components/UserAvatarIconButton/style.module.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-width: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
7
src/components/username.module.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import { Box, IconButton, Typography, useTheme } from '@mui/material'
|
import { Typography } from '@mui/material'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { getProfileRoute } from '../routes'
|
|
||||||
import { State } from '../store/rootReducer'
|
import { State } from '../store/rootReducer'
|
||||||
import { hexToNpub } from '../utils'
|
|
||||||
|
import styles from './username.module.scss'
|
||||||
|
import { AvatarIconButton } from './UserAvatarIconButton'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
username: string
|
username: string
|
||||||
@ -11,92 +11,36 @@ type Props = {
|
|||||||
handleClick: (event: React.MouseEvent<HTMLElement>) => void
|
handleClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component will be used for the displaying logged in user in AppBar.
|
||||||
|
* Clicking will open the menu.
|
||||||
|
*/
|
||||||
const Username = ({ username, avatarContent, handleClick }: Props) => {
|
const Username = ({ username, avatarContent, handleClick }: Props) => {
|
||||||
const hexKey = useSelector((state: State) => state.auth.usersPubkey)
|
const hexKey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<div className={styles.container}>
|
||||||
aria-label="account of current user"
|
|
||||||
aria-controls="menu-appbar"
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={handleClick}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={avatarContent}
|
|
||||||
alt="user-avatar"
|
|
||||||
className="profile-image"
|
|
||||||
style={{
|
|
||||||
borderWidth: '3px',
|
|
||||||
borderStyle: hexKey ? 'solid' : 'none',
|
|
||||||
borderColor: `#${hexKey?.substring(0, 6)}`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
sx={{
|
sx={{
|
||||||
color: '#3e3e3e',
|
fontWeight: 500,
|
||||||
padding: '0 8px',
|
fontSize: '14px',
|
||||||
|
color: 'var(--text-color)',
|
||||||
display: { xs: 'none', md: 'flex' }
|
display: { xs: 'none', md: 'flex' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{username}
|
{username}
|
||||||
</Typography>
|
</Typography>
|
||||||
</IconButton>
|
<AvatarIconButton
|
||||||
|
src={avatarContent}
|
||||||
|
hexKey={hexKey}
|
||||||
|
aria-label="account of current user"
|
||||||
|
aria-controls="menu-appbar"
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Username
|
export default Username
|
||||||
|
|
||||||
type UserProps = {
|
|
||||||
pubkey: string
|
|
||||||
name: string
|
|
||||||
image?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component will be used for the displaying username and profile picture.
|
|
||||||
* If image is not available, robohash image will be displayed
|
|
||||||
*/
|
|
||||||
export const UserComponent = ({ pubkey, name, image }: UserProps) => {
|
|
||||||
const theme = useTheme()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const npub = hexToNpub(pubkey)
|
|
||||||
const roboImage = `https://robohash.org/${npub}.png?set=set3`
|
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
// navigate to user's profile
|
|
||||||
navigate(getProfileRoute(pubkey))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{ display: 'flex', alignItems: 'center', gap: '10px', flexGrow: 1 }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={image || roboImage}
|
|
||||||
alt="User Image"
|
|
||||||
className="profile-image"
|
|
||||||
style={{
|
|
||||||
borderWidth: '3px',
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderColor: `#${pubkey.substring(0, 6)}`
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
component="label"
|
|
||||||
sx={{
|
|
||||||
textAlign: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: theme.palette.text.primary
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -1,55 +1,28 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
* {
|
||||||
font-weight: 500;
|
box-sizing: border-box;
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
/* display: flex;
|
|
||||||
place-items: center; */
|
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f4f4fb;
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qrzap {
|
.qrzap {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 80px;
|
top: 80px;
|
||||||
@ -57,21 +30,10 @@ button {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
#root {
|
||||||
:root {
|
min-height: 100vh;
|
||||||
color: #213547;
|
display: flex;
|
||||||
background-color: #ffffff;
|
flex-direction: column;
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
padding: 60px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-mobile {
|
.hide-mobile {
|
||||||
@ -90,6 +52,7 @@ button {
|
|||||||
* when this class is assigned to a component, user will not be able to select and copy the content from that component
|
* when this class is assigned to a component, user will not be able to select and copy the content from that component
|
||||||
*/
|
*/
|
||||||
.no-select {
|
.no-select {
|
||||||
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,3 +107,44 @@ button:disabled {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fonts */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
|
url('assets/fonts/roboto-medium.woff2') format('woff2'),
|
||||||
|
url('assets/fonts/roboto-medium.woff') format('woff');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url('assets/fonts/roboto-light.woff2') format('woff2'),
|
||||||
|
url('assets/fonts/roboto-light.woff') format('woff');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url('assets/fonts/roboto-bold.woff2') format('woff2'),
|
||||||
|
url('assets/fonts/roboto-bold.woff') format('woff');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
|
url('assets/fonts/roboto-regular.woff2') format('woff2'),
|
||||||
|
url('assets/fonts/roboto-regular.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
import { Box } from '@mui/material'
|
|
||||||
import Container from '@mui/material/Container'
|
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
@ -28,6 +26,8 @@ import {
|
|||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { useAppSelector } from '../hooks'
|
import { useAppSelector } from '../hooks'
|
||||||
import { SubCloser } from 'nostr-tools/abstract-pool'
|
import { SubCloser } from 'nostr-tools/abstract-pool'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { Footer } from '../components/Footer/Footer'
|
||||||
|
|
||||||
export const MainLayout = () => {
|
export const MainLayout = () => {
|
||||||
const dispatch: Dispatch = useDispatch()
|
const dispatch: Dispatch = useDispatch()
|
||||||
@ -136,7 +136,7 @@ export const MainLayout = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc(`Fetching user's app data`)
|
setLoadingSpinnerDesc(`Loading SIGit History`)
|
||||||
getUsersAppData()
|
getUsersAppData()
|
||||||
.then((appData) => {
|
.then((appData) => {
|
||||||
if (appData) {
|
if (appData) {
|
||||||
@ -152,19 +152,10 @@ export const MainLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBar />
|
<AppBar />
|
||||||
|
<main className={styles.main}>
|
||||||
<Box className="main">
|
<Outlet />
|
||||||
<Container
|
</main>
|
||||||
sx={{
|
<Footer />
|
||||||
position: 'relative',
|
|
||||||
maxWidth: {
|
|
||||||
xs: '550px'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
109
src/layouts/modal/index.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { Button, IconButton, Modal as ModalMui } from '@mui/material'
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
matchPath,
|
||||||
|
Outlet,
|
||||||
|
useLocation,
|
||||||
|
useNavigate
|
||||||
|
} from 'react-router-dom'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { appPublicRoutes } from '../../routes'
|
||||||
|
import { faClose } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import nostrImage from '../../assets/images/nostr.gif'
|
||||||
|
|
||||||
|
function useRouteMatch(patterns: readonly string[]) {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
|
for (let i = 0; i < patterns.length; i += 1) {
|
||||||
|
const pattern = patterns[i]
|
||||||
|
const possibleMatch = matchPath(pattern, pathname)
|
||||||
|
if (possibleMatch !== null) {
|
||||||
|
return possibleMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const tabs = [
|
||||||
|
{ to: appPublicRoutes.login, title: 'Login', label: 'Login' },
|
||||||
|
{ to: appPublicRoutes.register, title: 'Register', label: 'Register' },
|
||||||
|
{
|
||||||
|
to: appPublicRoutes.nostr,
|
||||||
|
title: 'Login',
|
||||||
|
sx: { padding: '10px' },
|
||||||
|
label: <img src={nostrImage} width="25" alt="nostr logo" height="25" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const routeMatch = useRouteMatch(tabs.map((t) => t.to))
|
||||||
|
const activeTab = routeMatch?.pattern?.path
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate(appPublicRoutes.landingPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalMui open={true} onClose={handleClose} aria-labelledby="modal-title">
|
||||||
|
<div className={styles.modal}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<h2 className={styles.title} id="modal-title">
|
||||||
|
{tabs.find((t) => activeTab === t.to)?.title}
|
||||||
|
</h2>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={{
|
||||||
|
fontSize: '18px',
|
||||||
|
color: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
padding: '8px 15px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
':hover': {
|
||||||
|
background: 'var(--primary-light)',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faClose} />
|
||||||
|
</IconButton>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className={styles.body}>
|
||||||
|
<ul>
|
||||||
|
{tabs.map((t) => {
|
||||||
|
return (
|
||||||
|
<li key={t.to}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={t.to}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
...(t?.sx ? t.sx : {})
|
||||||
|
}}
|
||||||
|
variant={
|
||||||
|
activeTab === t.to || t.to === appPublicRoutes.nostr
|
||||||
|
? 'contained'
|
||||||
|
: 'text'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
{activeTab === appPublicRoutes.login ||
|
||||||
|
activeTab === appPublicRoutes.register ? (
|
||||||
|
<div className={styles.comingSoon}>Coming soon!</div>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className={styles.footer}>Welcome to SIGit!</footer>
|
||||||
|
</div>
|
||||||
|
</ModalMui>
|
||||||
|
)
|
||||||
|
}
|
84
src/layouts/modal/style.module.scss
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
$default-modal-padding: 15px 25px;
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
|
||||||
|
background-color: $overlay-background-color;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $default-modal-padding;
|
||||||
|
border-bottom: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-gap: 15px;
|
||||||
|
padding: $default-modal-padding;
|
||||||
|
|
||||||
|
> ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
grid-gap: 10px;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
max-width: 65px;
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
border-left: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-gap: 15px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comingSoon {
|
||||||
|
padding: 15px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 15px;
|
||||||
|
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
@ -1,22 +1,8 @@
|
|||||||
@import '../colors.scss';
|
@import '../styles/colors.scss';
|
||||||
|
@import '../styles/sizes.scss';
|
||||||
|
|
||||||
@font-face {
|
.main {
|
||||||
font-family: 'Avenir';
|
flex-grow: 1;
|
||||||
font-weight: normal;
|
padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0;
|
||||||
src: local('Avenir'),
|
background-color: $body-background-color;
|
||||||
url(../assets/avenir-font/AvenirLTStd-Roman.otf) format('opentype');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Avenir';
|
|
||||||
font-weight: bold;
|
|
||||||
src: local('Avenir'),
|
|
||||||
url(../assets/avenir-font/AvenirLTStd-Black.otf) format('opentype');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Avenir';
|
|
||||||
font-weight: lighter;
|
|
||||||
src: local('Avenir'),
|
|
||||||
url(../assets/avenir-font/AvenirLTStd-Book.otf) format('opentype');
|
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ import { useSelector } from 'react-redux'
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserComponent } from '../../components/username'
|
import { UserAvatar } from '../../components/UserAvatar'
|
||||||
import { MetadataController, NostrController } from '../../controllers'
|
import { MetadataController, NostrController } from '../../controllers'
|
||||||
import { appPrivateRoutes } from '../../routes'
|
import { appPrivateRoutes } from '../../routes'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
@ -841,7 +841,7 @@ const DisplayUser = ({
|
|||||||
return (
|
return (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell className={styles.tableCell}>
|
<TableCell className={styles.tableCell}>
|
||||||
<UserComponent
|
<UserAvatar
|
||||||
pubkey={user.pubkey}
|
pubkey={user.pubkey}
|
||||||
name={
|
name={
|
||||||
userMeta?.display_name ||
|
userMeta?.display_name ||
|
||||||
@ -996,7 +996,7 @@ const SignerRow = ({
|
|||||||
sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}
|
sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}
|
||||||
>
|
>
|
||||||
<DragHandle />
|
<DragHandle />
|
||||||
<UserComponent
|
<UserAvatar
|
||||||
pubkey={user.pubkey}
|
pubkey={user.pubkey}
|
||||||
name={
|
name={
|
||||||
userMeta?.display_name ||
|
userMeta?.display_name ||
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -5,7 +5,7 @@ import { Event, kinds, verifyEvent } from 'nostr-tools'
|
|||||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { UserComponent } from '../../components/username'
|
import { UserAvatar } from '../../components/UserAvatar'
|
||||||
import { MetadataController } from '../../controllers'
|
import { MetadataController } from '../../controllers'
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||||
@ -18,6 +18,7 @@ import {
|
|||||||
shorten
|
shorten
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { Container } from '../../components/Container'
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -81,65 +82,63 @@ export const HomePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className={styles.container}>
|
||||||
<Box className={styles.container}>
|
<Box className={styles.header}>
|
||||||
<Box className={styles.header}>
|
<Typography variant="h3" className={styles.title}>
|
||||||
<Typography variant="h3" className={styles.title}>
|
Sigits
|
||||||
Sigits
|
</Typography>
|
||||||
</Typography>
|
{/* This is for desktop view */}
|
||||||
{/* This is for desktop view */}
|
<Box
|
||||||
<Box
|
className={styles.actionButtons}
|
||||||
className={styles.actionButtons}
|
sx={{
|
||||||
sx={{
|
display: {
|
||||||
display: {
|
xs: 'none',
|
||||||
xs: 'none',
|
md: 'flex'
|
||||||
md: 'flex'
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Upload />}
|
||||||
|
onClick={handleUploadClick}
|
||||||
>
|
>
|
||||||
<input
|
Upload
|
||||||
type="file"
|
</Button>
|
||||||
ref={fileInputRef}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<Upload />}
|
|
||||||
onClick={handleUploadClick}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
{/* This is for mobile view */}
|
|
||||||
<Box
|
|
||||||
className={styles.actionButtons}
|
|
||||||
sx={{
|
|
||||||
display: {
|
|
||||||
xs: 'flex',
|
|
||||||
md: 'none'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip title="Upload" arrow>
|
|
||||||
<Button variant="outlined" onClick={handleUploadClick}>
|
|
||||||
<Upload />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box className={styles.submissions}>
|
{/* This is for mobile view */}
|
||||||
{sigits.map((sigit, index) => (
|
<Box
|
||||||
<DisplaySigit
|
className={styles.actionButtons}
|
||||||
key={`sigit-${index}`}
|
sx={{
|
||||||
meta={sigit}
|
display: {
|
||||||
profiles={profiles}
|
xs: 'flex',
|
||||||
setProfiles={setProfiles}
|
md: 'none'
|
||||||
/>
|
}
|
||||||
))}
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title="Upload" arrow>
|
||||||
|
<Button variant="outlined" onClick={handleUploadClick}>
|
||||||
|
<Upload />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
<Box className={styles.submissions}>
|
||||||
|
{sigits.map((sigit, index) => (
|
||||||
|
<DisplaySigit
|
||||||
|
key={`sigit-${index}`}
|
||||||
|
meta={sigit}
|
||||||
|
profiles={profiles}
|
||||||
|
setProfiles={setProfiles}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +290,7 @@ const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
|||||||
(function () {
|
(function () {
|
||||||
const profile = profiles[submittedBy]
|
const profile = profiles[submittedBy]
|
||||||
return (
|
return (
|
||||||
<UserComponent
|
<UserAvatar
|
||||||
pubkey={submittedBy}
|
pubkey={submittedBy}
|
||||||
name={
|
name={
|
||||||
profile?.display_name ||
|
profile?.display_name ||
|
||||||
@ -371,7 +370,7 @@ const DisplaySigner = ({ meta, profile, pubkey }: DisplaySignerProps) => {
|
|||||||
<Typography variant="button" className={styles.status}>
|
<Typography variant="button" className={styles.status}>
|
||||||
{signStatus}
|
{signStatus}
|
||||||
</Typography>
|
</Typography>
|
||||||
<UserComponent
|
<UserAvatar
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
name={
|
name={
|
||||||
profile?.display_name ||
|
profile?.display_name ||
|
||||||
|
@ -2,94 +2,91 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
padding: 10px;
|
}
|
||||||
margin-top: 10px;
|
|
||||||
background: var(--mui-palette-background-paper);
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--mui-palette-primary-light);
|
color: var(--mui-palette-primary-light);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.actionButtons {
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submissions {
|
.actionButtons {
|
||||||
display: flex;
|
justify-content: center;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.submissions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: #efeae6;
|
||||||
|
border-radius: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.titleBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 4;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
background-color: #efeae6;
|
padding: 10px;
|
||||||
border-radius: 1rem;
|
background-color: #cdc8c499;
|
||||||
cursor: pointer;
|
border-top-left-radius: inherit;
|
||||||
|
|
||||||
.titleBox {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 4;
|
justify-content: center;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow-wrap: anywhere;
|
color: var(--mui-palette-primary-light);
|
||||||
gap: 10px;
|
font-size: 1.5rem;
|
||||||
padding: 10px;
|
|
||||||
background-color: #cdc8c499;
|
|
||||||
border-top-left-radius: inherit;
|
|
||||||
|
|
||||||
.title {
|
svg {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.signers {
|
.date {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
flex: 6;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
align-items: center;
|
||||||
padding: 10px;
|
|
||||||
color: var(--mui-palette-primary-light);
|
color: var(--mui-palette-primary-light);
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
.signerItem {
|
svg {
|
||||||
display: flex;
|
font-size: 1rem;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
}
|
||||||
gap: 10px;
|
}
|
||||||
|
|
||||||
.status {
|
.signers {
|
||||||
border-radius: 2rem;
|
display: flex;
|
||||||
width: 100px;
|
flex-direction: column;
|
||||||
text-align: center;
|
flex: 6;
|
||||||
background-color: var(--mui-palette-info-light);
|
justify-content: center;
|
||||||
}
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--mui-palette-primary-light);
|
||||||
|
|
||||||
|
.signerItem {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.status {
|
||||||
|
border-radius: 2rem;
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--mui-palette-info-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
import { Box, Button, Typography, useTheme } from '@mui/material'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
|
||||||
import { appPublicRoutes } from '../../routes'
|
|
||||||
import { State } from '../../store/rootReducer'
|
|
||||||
import { saveVisitedLink } from '../../utils'
|
|
||||||
import styles from './style.module.scss'
|
|
||||||
|
|
||||||
const bodyBackgroundColor = document.body.style.backgroundColor
|
|
||||||
|
|
||||||
export const LandingPage = () => {
|
|
||||||
const authState = useSelector((state: State) => state.auth)
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const theme = useTheme()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
saveVisitedLink(location.pathname, location.search)
|
|
||||||
}, [location])
|
|
||||||
|
|
||||||
const onSignInClick = async () => {
|
|
||||||
navigate(appPublicRoutes.login)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.landingPage}>
|
|
||||||
<Box>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 5,
|
|
||||||
color: bodyBackgroundColor
|
|
||||||
? theme.palette.getContrastText(bodyBackgroundColor)
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
variant="h4"
|
|
||||||
>
|
|
||||||
Secure Document Signing
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
color: bodyBackgroundColor
|
|
||||||
? theme.palette.getContrastText(bodyBackgroundColor)
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
variant="body1"
|
|
||||||
>
|
|
||||||
SIGit is an open-source and self-hostable solution for secure document
|
|
||||||
signing and verification. Code is MIT licenced and available at{' '}
|
|
||||||
<a
|
|
||||||
className="bold-link"
|
|
||||||
target="_blank"
|
|
||||||
href="https://git.sigit.io/sig/it"
|
|
||||||
>
|
|
||||||
https://git.sigit.io/sig/it
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
SIGit lets you Create, Sign and Verify from any device with a browser.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Unlike other solutions, SIGit is totally private - files are encrypted
|
|
||||||
locally, and can only be exported by named recipients.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Anyone can <Link to={appPublicRoutes.verify}>VERIFY</Link> the
|
|
||||||
exported document.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{!authState?.loggedIn && (
|
|
||||||
<div className={styles.loginBottomBar}>
|
|
||||||
<Button
|
|
||||||
className={styles.loginBtn}
|
|
||||||
variant="contained"
|
|
||||||
onClick={onSignInClick}
|
|
||||||
>
|
|
||||||
GET STARTED
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
167
src/pages/landing/index.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { Box, Button } from '@mui/material'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { appPublicRoutes } from '../../routes'
|
||||||
|
import { saveVisitedLink } from '../../utils'
|
||||||
|
import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
|
||||||
|
import { Container } from '../../components/Container'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import bg_l from '../../assets/images/bg_l.svg'
|
||||||
|
import bg_r from '../../assets/images/bg_r.svg'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faOsi } from '@fortawesome/free-brands-svg-icons'
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faMobileScreenButton,
|
||||||
|
faShieldHeart,
|
||||||
|
faSlash,
|
||||||
|
faUsers,
|
||||||
|
faWifi
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack'
|
||||||
|
|
||||||
|
export const LandingPage = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const onSignInClick = async () => {
|
||||||
|
navigate(appPublicRoutes.nostr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
icon: <FontAwesomeIcon width={25} height={25} icon={faOsi} />,
|
||||||
|
title: <>Open Source</>,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Code is MIT licenced and available at{' '}
|
||||||
|
<a href="https://git.nostrdev.com/sigit/sigit.io">
|
||||||
|
https://git.nostrdev.com/sigit/sigit.io
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<FontAwesomeIcon width={25} height={25} icon={faMobileScreenButton} />
|
||||||
|
),
|
||||||
|
title: <>Multi-Device</>,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Create, Sign and Verify documents and files from any device with a
|
||||||
|
browser.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FontAwesomeIcon width={25} height={25} icon={faShieldHeart} />,
|
||||||
|
title: <>Secure & Private</>,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Documents are encrypted locally and can be accessed only by named
|
||||||
|
recipients.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FontAwesomeIcon width={25} height={25} icon={faCheck} />,
|
||||||
|
title: <>Verifiable</>,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Thanks to Schnorr Signatures and Web of Trust, SIGit is far more
|
||||||
|
auditable than traditional server-based offerings.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<FontAwesomeIconStack>
|
||||||
|
<FontAwesomeIcon width={25} height={25} icon={faSlash} />
|
||||||
|
<FontAwesomeIcon width={25} height={25} icon={faWifi} />
|
||||||
|
</FontAwesomeIconStack>
|
||||||
|
),
|
||||||
|
title: <>Works Offline</>,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Presuming you have a hardware signing device, it is possible to
|
||||||
|
complete a SIGit round without an internet connection.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FontAwesomeIcon width={25} height={25} icon={faUsers} />,
|
||||||
|
title: <>Multi-Party Signing</>,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Choose any number of Signers and Viewers, track the signature status,
|
||||||
|
send reminders, get notifications on completion.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveVisitedLink(location.pathname, location.search)
|
||||||
|
}, [location])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.background}>
|
||||||
|
<div
|
||||||
|
className={`${styles.backgroundBlob} ${styles.backgroundBlobLeft}`}
|
||||||
|
style={{ backgroundImage: `url("${bg_l}")` }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={`${styles.backgroundBlob} ${styles.backgroundBlobRight}`}
|
||||||
|
style={{ backgroundImage: `url("${bg_r}")` }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<Container className={styles.container}>
|
||||||
|
<img className={styles.logo} src="/logo.svg" alt="Logo" width={300} />
|
||||||
|
<div className={styles.titleSection}>
|
||||||
|
<h1 className={styles.title}>
|
||||||
|
Secure & Private Document Signing
|
||||||
|
</h1>
|
||||||
|
<p className={styles.subTitle}>
|
||||||
|
An open-source and self-hostable solution for secure document
|
||||||
|
signing and verification.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Box
|
||||||
|
display={'grid'}
|
||||||
|
gap={'25px'}
|
||||||
|
sx={{
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: '1fr',
|
||||||
|
sm: 'repeat(2, 1fr)',
|
||||||
|
xl: 'repeat(3, 1fr)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cards.map((c, i) => (
|
||||||
|
<CardComponent key={i} {...c} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<p className={styles.description}>
|
||||||
|
SIGit is a secure & private document signing service where you can
|
||||||
|
create, sign, and verify any document from any device with a browser.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
fontSize: '22px',
|
||||||
|
padding: '16px 32px',
|
||||||
|
backgroundColor: 'var(--secondary-main)'
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
onClick={onSignInClick}
|
||||||
|
>
|
||||||
|
GET STARTED
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,29 +1,70 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
@import '../../styles/sizes.scss';
|
||||||
|
|
||||||
.landingPage {
|
.background {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 45px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
padding-block: 50px;
|
||||||
|
padding-inline: 50px + $default-container-padding-inline;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $text-color;
|
|
||||||
min-height: 80vh;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 20px;
|
}
|
||||||
|
|
||||||
.loginBottomBar {
|
.logo {
|
||||||
display: flex;
|
width: 100%;
|
||||||
justify-content: center;
|
max-width: 500px;
|
||||||
align-items: center;
|
}
|
||||||
padding: 10px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.7);
|
.titleSection {
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.096);
|
display: flex;
|
||||||
position: fixed;
|
flex-direction: column;
|
||||||
bottom: 0;
|
gap: 15px;
|
||||||
left: 0;
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backgroundBlob {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.05;
|
||||||
|
width: 13%;
|
||||||
|
height: 349px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: no-repeat center / contain;
|
||||||
|
|
||||||
|
&Right {
|
||||||
|
top: 50px;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
background-position: right;
|
||||||
|
}
|
||||||
|
|
||||||
.loginBtn {
|
&Left {
|
||||||
// margin-top: 20px;
|
bottom: 50px;
|
||||||
min-width: 200px;
|
left: 0;
|
||||||
}
|
background-position: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,380 +1,30 @@
|
|||||||
import { Box, Button, TextField, Typography } from '@mui/material'
|
import { Button, TextField } from '@mui/material'
|
||||||
import { getPublicKey, nip19 } from 'nostr-tools'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
|
||||||
import {
|
|
||||||
AuthController,
|
|
||||||
MetadataController,
|
|
||||||
NostrController
|
|
||||||
} from '../../controllers'
|
|
||||||
import {
|
|
||||||
updateKeyPair,
|
|
||||||
updateLoginMethod,
|
|
||||||
updateNsecbunkerPubkey,
|
|
||||||
updateNsecbunkerRelays
|
|
||||||
} from '../../store/actions'
|
|
||||||
import { LoginMethods } from '../../store/auth/types'
|
|
||||||
import { Dispatch } from '../../store/store'
|
|
||||||
import { npubToHex, queryNip05 } from '../../utils'
|
|
||||||
import styles from './style.module.scss'
|
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
|
||||||
import { NIP05_REGEX } from '../../constants'
|
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const [searchParams] = useSearchParams()
|
// TODO: All fields, buttons are disabled
|
||||||
|
// Feature not implemented
|
||||||
const dispatch: Dispatch = useDispatch()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const authController = new AuthController()
|
|
||||||
const metadataController = new MetadataController()
|
|
||||||
const nostrController = NostrController.getInstance()
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
|
||||||
const [inputValue, setInputValue] = useState('')
|
|
||||||
const [authUrl, setAuthUrl] = useState<string>()
|
|
||||||
|
|
||||||
const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
|
|
||||||
useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsNostrExtensionAvailable(!!window.nostr)
|
|
||||||
}, 500)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call login function when enter is pressed
|
|
||||||
*/
|
|
||||||
const handleInputKeyDown = (event: any) => {
|
|
||||||
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
|
||||||
event.preventDefault()
|
|
||||||
login()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigateAfterLogin = (path: string) => {
|
|
||||||
const callbackPath = searchParams.get('callbackPath')
|
|
||||||
|
|
||||||
if (callbackPath) {
|
|
||||||
// base64 decoded path
|
|
||||||
const path = atob(callbackPath)
|
|
||||||
navigate(path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginWithExtension = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
|
||||||
|
|
||||||
nostrController
|
|
||||||
.capturePublicKey()
|
|
||||||
.then(async (pubkey) => {
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.extension))
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
const redirectPath =
|
|
||||||
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error capturing public key from nostr extension: ' + err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login with NSEC or HEX private key
|
|
||||||
* @param privateKey in HEX format
|
|
||||||
*/
|
|
||||||
const loginWithNsec = async (privateKey?: Uint8Array) => {
|
|
||||||
let nsec = ''
|
|
||||||
|
|
||||||
if (privateKey) {
|
|
||||||
nsec = nip19.nsecEncode(privateKey)
|
|
||||||
} else {
|
|
||||||
nsec = inputValue
|
|
||||||
|
|
||||||
try {
|
|
||||||
privateKey = nip19.decode(nsec).data as Uint8Array
|
|
||||||
} catch (err) {
|
|
||||||
toast.error(`Error decoding the nsec. ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!privateKey) {
|
|
||||||
toast.error(
|
|
||||||
'Snap, we failed to convert the private key you provided. Please make sure key is valid.'
|
|
||||||
)
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const publickey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
updateKeyPair({
|
|
||||||
private: nsec,
|
|
||||||
public: publickey
|
|
||||||
})
|
|
||||||
)
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.privateKey))
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
|
|
||||||
const redirectPath = await authController
|
|
||||||
.authAndGetMetadataAndRelaysMap(publickey)
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error occurred in authentication: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginWithNsecBunker = async () => {
|
|
||||||
let relays: string[] | undefined
|
|
||||||
let pubkey: string | undefined
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const displayError = (message: string) => {
|
|
||||||
toast.error(message)
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputValue.match(NIP05_REGEX)) {
|
|
||||||
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
|
||||||
toast.error('An error occurred while querying nip05 profile: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (nip05Profile) {
|
|
||||||
pubkey = nip05Profile.pubkey
|
|
||||||
relays = nip05Profile.relays
|
|
||||||
}
|
|
||||||
} else if (inputValue.startsWith('npub')) {
|
|
||||||
pubkey = nip19.decode(inputValue).data as string
|
|
||||||
const metadataEvent = await metadataController
|
|
||||||
.findMetadata(pubkey)
|
|
||||||
.catch(() => {
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!metadataEvent) {
|
|
||||||
return displayError('metadata not found!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataContent =
|
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
|
||||||
|
|
||||||
if (!metadataContent?.nip05) {
|
|
||||||
return displayError('nip05 not present in metadata')
|
|
||||||
}
|
|
||||||
|
|
||||||
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
|
||||||
toast.error('An error occurred while querying nip05 profile: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (nip05Profile) {
|
|
||||||
if (nip05Profile.pubkey !== pubkey) {
|
|
||||||
return displayError(
|
|
||||||
'pubkey in nip05 does not match with provided npub'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
relays = nip05Profile.relays
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!relays || relays.length === 0) {
|
|
||||||
return displayError('No relay found for nsecbunker')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pubkey) {
|
|
||||||
return displayError('pubkey not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Initializing nsecBunker')
|
|
||||||
await nostrController.nsecBunkerInit(relays)
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Creating nsecbunker singer')
|
|
||||||
await nostrController
|
|
||||||
.createNsecBunkerSigner(pubkey)
|
|
||||||
.then(async (signer) => {
|
|
||||||
signer.on('authUrl', (url: string) => {
|
|
||||||
setAuthUrl(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
|
||||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
|
||||||
dispatch(updateNsecbunkerRelays(relays))
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
|
|
||||||
const redirectPath = await authController
|
|
||||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error occurred in authentication: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(
|
|
||||||
'An error occurred while creating nsecbunker signer: ' + err
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginWithBunkerConnectionString = async () => {
|
|
||||||
// Extract the key
|
|
||||||
const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length
|
|
||||||
const keyEndIndex = inputValue.indexOf('?relay=')
|
|
||||||
const key = inputValue.substring(keyStartIndex, keyEndIndex)
|
|
||||||
|
|
||||||
const pubkey = npubToHex(key)
|
|
||||||
|
|
||||||
if (!pubkey) {
|
|
||||||
toast.error('Invalid pubkey in bunker connection string.')
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the relay value
|
|
||||||
const relayIndex = inputValue.indexOf('relay=')
|
|
||||||
const relay = inputValue.substring(
|
|
||||||
relayIndex + 'relay='.length,
|
|
||||||
inputValue.length
|
|
||||||
)
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Initializing bunker NDK')
|
|
||||||
|
|
||||||
await nostrController.nsecBunkerInit([relay])
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Creating remote signer')
|
|
||||||
await nostrController
|
|
||||||
.createNsecBunkerSigner(pubkey)
|
|
||||||
.then(async (signer) => {
|
|
||||||
signer.on('authUrl', (url: string) => {
|
|
||||||
setAuthUrl(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
|
||||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
|
||||||
dispatch(updateNsecbunkerRelays([relay]))
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
|
|
||||||
const redirectPath = await authController
|
|
||||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error occurred in authentication: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(
|
|
||||||
'An error occurred while creating nsecbunker signer: ' + err
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const login = () => {
|
|
||||||
if (inputValue.startsWith('bunker://')) {
|
|
||||||
return loginWithBunkerConnectionString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputValue.startsWith('nsec')) {
|
|
||||||
return loginWithNsec()
|
|
||||||
}
|
|
||||||
if (inputValue.startsWith('npub')) {
|
|
||||||
return loginWithNsecBunker()
|
|
||||||
}
|
|
||||||
if (inputValue.match(NIP05_REGEX)) {
|
|
||||||
return loginWithNsecBunker()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if maybe hex nsec
|
|
||||||
try {
|
|
||||||
const privateKey = hexToBytes(inputValue)
|
|
||||||
const publickey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
if (publickey) return loginWithNsec(privateKey)
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('err', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authUrl) {
|
|
||||||
return (
|
|
||||||
<iframe
|
|
||||||
title="Nsecbunker auth"
|
|
||||||
src={authUrl}
|
|
||||||
width="100%"
|
|
||||||
height="500px"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
<TextField
|
||||||
<div className={styles.loginPage}>
|
label="Email"
|
||||||
<Typography variant="h4">Welcome to Sigit</Typography>
|
placeholder="Your email address"
|
||||||
<TextField
|
fullWidth
|
||||||
onKeyDown={handleInputKeyDown}
|
margin="dense"
|
||||||
label="nip05 login / nip46 bunker string"
|
autoComplete="username"
|
||||||
value={inputValue}
|
disabled
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
/>
|
||||||
sx={{ width: '100%', mt: 2 }}
|
<TextField
|
||||||
/>
|
label="Password"
|
||||||
{isNostrExtensionAvailable && (
|
placeholder="Your password"
|
||||||
<Button onClick={loginWithExtension} variant="text">
|
fullWidth
|
||||||
Login with extension
|
margin="dense"
|
||||||
</Button>
|
autoComplete="current-password"
|
||||||
)}
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
<Button variant="contained" fullWidth disabled>
|
||||||
<Button disabled={!inputValue} onClick={login} variant="contained">
|
Login
|
||||||
Login
|
</Button>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
@import '../../colors.scss';
|
|
||||||
|
|
||||||
.loginPage {
|
|
||||||
color: $text-color;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
400
src/pages/nostr/index.tsx
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
import { Button, Divider, TextField } from '@mui/material'
|
||||||
|
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
|
import {
|
||||||
|
AuthController,
|
||||||
|
MetadataController,
|
||||||
|
NostrController
|
||||||
|
} from '../../controllers'
|
||||||
|
import {
|
||||||
|
updateKeyPair,
|
||||||
|
updateLoginMethod,
|
||||||
|
updateNsecbunkerPubkey,
|
||||||
|
updateNsecbunkerRelays
|
||||||
|
} from '../../store/actions'
|
||||||
|
import { LoginMethods } from '../../store/auth/types'
|
||||||
|
import { Dispatch } from '../../store/store'
|
||||||
|
import { npubToHex, queryNip05 } from '../../utils'
|
||||||
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
import { NIP05_REGEX } from '../../constants'
|
||||||
|
|
||||||
|
import styles from './styles.module.scss'
|
||||||
|
|
||||||
|
export const Nostr = () => {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const dispatch: Dispatch = useDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const authController = new AuthController()
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const [authUrl, setAuthUrl] = useState<string>()
|
||||||
|
|
||||||
|
const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsNostrExtensionAvailable(!!window.nostr)
|
||||||
|
}, 500)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call login function when enter is pressed
|
||||||
|
*/
|
||||||
|
const handleInputKeyDown = (event: any) => {
|
||||||
|
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
||||||
|
event.preventDefault()
|
||||||
|
login()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateAfterLogin = (path: string) => {
|
||||||
|
const callbackPath = searchParams.get('callbackPath')
|
||||||
|
|
||||||
|
if (callbackPath) {
|
||||||
|
// base64 decoded path
|
||||||
|
const path = atob(callbackPath)
|
||||||
|
navigate(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWithExtension = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||||
|
|
||||||
|
nostrController
|
||||||
|
.capturePublicKey()
|
||||||
|
.then(async (pubkey) => {
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.extension))
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
const redirectPath =
|
||||||
|
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error capturing public key from nostr extension: ' + err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login with NSEC or HEX private key
|
||||||
|
* @param privateKey in HEX format
|
||||||
|
*/
|
||||||
|
const loginWithNsec = async (privateKey?: Uint8Array) => {
|
||||||
|
let nsec = ''
|
||||||
|
|
||||||
|
if (privateKey) {
|
||||||
|
nsec = nip19.nsecEncode(privateKey)
|
||||||
|
} else {
|
||||||
|
nsec = inputValue
|
||||||
|
|
||||||
|
try {
|
||||||
|
privateKey = nip19.decode(nsec).data as Uint8Array
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Error decoding the nsec. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
toast.error(
|
||||||
|
'Snap, we failed to convert the private key you provided. Please make sure key is valid.'
|
||||||
|
)
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const publickey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
updateKeyPair({
|
||||||
|
private: nsec,
|
||||||
|
public: publickey
|
||||||
|
})
|
||||||
|
)
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.privateKey))
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
|
const redirectPath = await authController
|
||||||
|
.authAndGetMetadataAndRelaysMap(publickey)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error occurred in authentication: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWithNsecBunker = async () => {
|
||||||
|
let relays: string[] | undefined
|
||||||
|
let pubkey: string | undefined
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const displayError = (message: string) => {
|
||||||
|
toast.error(message)
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue.match(NIP05_REGEX)) {
|
||||||
|
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
||||||
|
toast.error('An error occurred while querying nip05 profile: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nip05Profile) {
|
||||||
|
pubkey = nip05Profile.pubkey
|
||||||
|
relays = nip05Profile.relays
|
||||||
|
}
|
||||||
|
} else if (inputValue.startsWith('npub')) {
|
||||||
|
pubkey = nip19.decode(inputValue).data as string
|
||||||
|
const metadataEvent = await metadataController
|
||||||
|
.findMetadata(pubkey)
|
||||||
|
.catch(() => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!metadataEvent) {
|
||||||
|
return displayError('metadata not found!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(metadataEvent)
|
||||||
|
|
||||||
|
if (!metadataContent?.nip05) {
|
||||||
|
return displayError('nip05 not present in metadata')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
||||||
|
toast.error('An error occurred while querying nip05 profile: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nip05Profile) {
|
||||||
|
if (nip05Profile.pubkey !== pubkey) {
|
||||||
|
return displayError(
|
||||||
|
'pubkey in nip05 does not match with provided npub'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
relays = nip05Profile.relays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relays || relays.length === 0) {
|
||||||
|
return displayError('No relay found for nsecbunker')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
return displayError('pubkey not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Initializing nsecBunker')
|
||||||
|
await nostrController.nsecBunkerInit(relays)
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Creating nsecbunker singer')
|
||||||
|
await nostrController
|
||||||
|
.createNsecBunkerSigner(pubkey)
|
||||||
|
.then(async (signer) => {
|
||||||
|
signer.on('authUrl', (url: string) => {
|
||||||
|
setAuthUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||||
|
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||||
|
dispatch(updateNsecbunkerRelays(relays))
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
|
const redirectPath = await authController
|
||||||
|
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error occurred in authentication: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(
|
||||||
|
'An error occurred while creating nsecbunker signer: ' + err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWithBunkerConnectionString = async () => {
|
||||||
|
// Extract the key
|
||||||
|
const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length
|
||||||
|
const keyEndIndex = inputValue.indexOf('?relay=')
|
||||||
|
const key = inputValue.substring(keyStartIndex, keyEndIndex)
|
||||||
|
|
||||||
|
const pubkey = npubToHex(key)
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
toast.error('Invalid pubkey in bunker connection string.')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the relay value
|
||||||
|
const relayIndex = inputValue.indexOf('relay=')
|
||||||
|
const relay = inputValue.substring(
|
||||||
|
relayIndex + 'relay='.length,
|
||||||
|
inputValue.length
|
||||||
|
)
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Initializing bunker NDK')
|
||||||
|
|
||||||
|
await nostrController.nsecBunkerInit([relay])
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Creating remote signer')
|
||||||
|
await nostrController
|
||||||
|
.createNsecBunkerSigner(pubkey)
|
||||||
|
.then(async (signer) => {
|
||||||
|
signer.on('authUrl', (url: string) => {
|
||||||
|
setAuthUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||||
|
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||||
|
dispatch(updateNsecbunkerRelays([relay]))
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
|
const redirectPath = await authController
|
||||||
|
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error occurred in authentication: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(
|
||||||
|
'An error occurred while creating nsecbunker signer: ' + err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
if (inputValue.startsWith('bunker://')) {
|
||||||
|
return loginWithBunkerConnectionString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue.startsWith('nsec')) {
|
||||||
|
return loginWithNsec()
|
||||||
|
}
|
||||||
|
if (inputValue.startsWith('npub')) {
|
||||||
|
return loginWithNsecBunker()
|
||||||
|
}
|
||||||
|
if (inputValue.match(NIP05_REGEX)) {
|
||||||
|
return loginWithNsecBunker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if maybe hex nsec
|
||||||
|
try {
|
||||||
|
const privateKey = hexToBytes(inputValue)
|
||||||
|
const publickey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
if (publickey) return loginWithNsec(privateKey)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('err', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(
|
||||||
|
'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authUrl) {
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
title="Nsecbunker auth"
|
||||||
|
src={authUrl}
|
||||||
|
width="100%"
|
||||||
|
height="500px"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
|
|
||||||
|
{isNostrExtensionAvailable && (
|
||||||
|
<>
|
||||||
|
<label className={styles.label} htmlFor="extension-login">
|
||||||
|
Login by using a browser extension
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
id="extension-login"
|
||||||
|
onClick={loginWithExtension}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Extension Login
|
||||||
|
</Button>
|
||||||
|
<Divider
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
or
|
||||||
|
</Divider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
label="nip05 login / nip46 bunker string"
|
||||||
|
helperText="Private key (Not recommended)"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={!inputValue}
|
||||||
|
onClick={login}
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
1
src/pages/nostr/styles.module.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import '../../styles/form.scss';
|
@ -14,6 +14,7 @@ import { State } from '../../store/rootReducer'
|
|||||||
import { NostrJoiningBlock, ProfileMetadata } from '../../types'
|
import { NostrJoiningBlock, ProfileMetadata } from '../../types'
|
||||||
import { getRoboHashPicture, hexToNpub, shorten } from '../../utils'
|
import { getRoboHashPicture, hexToNpub, shorten } from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { Container } from '../../components/Container'
|
||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -155,7 +156,7 @@ export const ProfilePage = () => {
|
|||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
{pubkey && (
|
{pubkey && (
|
||||||
<Box className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<Box
|
<Box
|
||||||
className={`${styles.banner} ${!profileMetadata || !profileMetadata.banner ? styles.noImage : ''}`}
|
className={`${styles.banner} ${!profileMetadata || !profileMetadata.banner ? styles.noImage : ''}`}
|
||||||
>
|
>
|
||||||
@ -278,7 +279,7 @@ export const ProfilePage = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Container>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
40
src/pages/register/index.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Button, TextField } from '@mui/material'
|
||||||
|
|
||||||
|
export const Register = () => {
|
||||||
|
// TODO: All fields, buttons are disabled
|
||||||
|
// Feature not implemented
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
label="Email"
|
||||||
|
placeholder="Your email address"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="username"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Confirm password"
|
||||||
|
placeholder="Re-type your password"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="contained" fullWidth disabled>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -12,6 +12,7 @@ import { useSelector } from 'react-redux'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
|
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
|
import { Container } from '../../components/Container'
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -42,55 +43,56 @@ export const SettingsPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<Container>
|
||||||
sx={{
|
<List
|
||||||
width: '100%',
|
sx={{
|
||||||
bgcolor: 'background.paper',
|
width: '100%',
|
||||||
marginTop: 2
|
bgcolor: 'background.paper'
|
||||||
}}
|
}}
|
||||||
subheader={
|
subheader={
|
||||||
<ListSubheader
|
<ListSubheader
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
borderBottom: '0.5px solid',
|
borderBottom: '0.5px solid',
|
||||||
paddingBottom: 2,
|
paddingBottom: 2,
|
||||||
paddingTop: 2
|
paddingTop: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => {
|
||||||
|
navigate(getProfileSettingsRoute(usersPubkey!))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Settings
|
<ListItemIcon>
|
||||||
</ListSubheader>
|
<AccountCircleIcon />
|
||||||
}
|
</ListItemIcon>
|
||||||
>
|
{listItem('Profile')}
|
||||||
<ListItemButton
|
</ListItemButton>
|
||||||
onClick={() => {
|
<ListItemButton
|
||||||
navigate(getProfileSettingsRoute(usersPubkey!))
|
onClick={() => {
|
||||||
}}
|
navigate(appPrivateRoutes.relays)
|
||||||
>
|
}}
|
||||||
<ListItemIcon>
|
>
|
||||||
<AccountCircleIcon />
|
<ListItemIcon>
|
||||||
</ListItemIcon>
|
<RouterIcon />
|
||||||
{listItem('Profile')}
|
</ListItemIcon>
|
||||||
</ListItemButton>
|
{listItem('Relays')}
|
||||||
<ListItemButton
|
</ListItemButton>
|
||||||
onClick={() => {
|
<ListItemButton
|
||||||
navigate(appPrivateRoutes.relays)
|
onClick={() => {
|
||||||
}}
|
navigate(appPrivateRoutes.cacheSettings)
|
||||||
>
|
}}
|
||||||
<ListItemIcon>
|
>
|
||||||
<RouterIcon />
|
<ListItemIcon>
|
||||||
</ListItemIcon>
|
<CachedIcon />
|
||||||
{listItem('Relays')}
|
</ListItemIcon>
|
||||||
</ListItemButton>
|
{listItem('Local Cache')}
|
||||||
<ListItemButton
|
</ListItemButton>
|
||||||
onClick={() => {
|
</List>
|
||||||
navigate(appPrivateRoutes.cacheSettings)
|
</Container>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<CachedIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
{listItem('Local Cache')}
|
|
||||||
</ListItemButton>
|
|
||||||
</List>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
5
src/pages/settings/cache/index.tsx
vendored
@ -13,6 +13,7 @@ import { useState } from 'react'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { localCache } from '../../../services'
|
import { localCache } from '../../../services'
|
||||||
import { LoadingSpinner } from '../../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../../components/LoadingSpinner'
|
||||||
|
import { Container } from '../../../components/Container'
|
||||||
|
|
||||||
export const CacheSettingsPage = () => {
|
export const CacheSettingsPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -49,7 +50,7 @@ export const CacheSettingsPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
<List
|
<List
|
||||||
sx={{
|
sx={{
|
||||||
@ -91,6 +92,6 @@ export const CacheSettingsPage = () => {
|
|||||||
{listItem('Clear Cache')}
|
{listItem('Clear Cache')}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import { LoadingSpinner } from '../../../components/LoadingSpinner'
|
|||||||
import { LoginMethods } from '../../../store/auth/types'
|
import { LoginMethods } from '../../../store/auth/types'
|
||||||
import { SmartToy } from '@mui/icons-material'
|
import { SmartToy } from '@mui/icons-material'
|
||||||
import { getRoboHashPicture } from '../../../utils'
|
import { getRoboHashPicture } from '../../../utils'
|
||||||
|
import { Container } from '../../../components/Container'
|
||||||
|
|
||||||
export const ProfileSettingsPage = () => {
|
export const ProfileSettingsPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -271,7 +272,7 @@ export const ProfileSettingsPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
<div className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<List
|
<List
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
@ -380,7 +381,7 @@ export const ProfileSettingsPage = () => {
|
|||||||
SAVE
|
SAVE
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
shorten
|
shorten
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { Container } from '../../../components/Container'
|
||||||
|
|
||||||
export const RelaysPage = () => {
|
export const RelaysPage = () => {
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
@ -314,7 +315,7 @@ export const RelaysPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<Box className={styles.relayAddContainer}>
|
<Box className={styles.relayAddContainer}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Add new relay"
|
label="Add new relay"
|
||||||
@ -512,6 +513,6 @@ export const RelaysPage = () => {
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
@import '../../../colors.scss';
|
@import '../../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin-top: 25px;
|
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
|
||||||
.relayURItextfield {
|
.relayURItextfield {
|
||||||
@ -93,11 +92,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connectionStatusConnected {
|
.connectionStatusConnected {
|
||||||
background-color: $review-feedback-correct;
|
background-color: $relay-status-connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionStatusNotConnected {
|
.connectionStatusNotConnected {
|
||||||
background-color: $review-feedback-incorrect;
|
background-color: $relay-status-notconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionStatusUnknown {
|
.connectionStatusUnknown {
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { DisplayMeta } from './internal/displayMeta'
|
import { DisplayMeta } from './internal/displayMeta'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { Container } from '../../components/Container'
|
||||||
enum SignedStatus {
|
enum SignedStatus {
|
||||||
Fully_Signed,
|
Fully_Signed,
|
||||||
User_Is_Next_Signer,
|
User_Is_Next_Signer,
|
||||||
@ -850,7 +851,7 @@ export const SignPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
<Box className={styles.container}>
|
<Container className={styles.container}>
|
||||||
{displayInput && (
|
{displayInput && (
|
||||||
<>
|
<>
|
||||||
<Typography component="label" variant="h6">
|
<Typography component="label" variant="h6">
|
||||||
@ -916,7 +917,7 @@ export const SignPage = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ import saveAs from 'file-saver'
|
|||||||
import { kinds, Event } from 'nostr-tools'
|
import { kinds, Event } from 'nostr-tools'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { UserComponent } from '../../../components/username'
|
import { UserAvatar } from '../../../components/UserAvatar'
|
||||||
import { MetadataController } from '../../../controllers'
|
import { MetadataController } from '../../../controllers'
|
||||||
import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils'
|
import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils'
|
||||||
import styles from '../style.module.scss'
|
import styles from '../style.module.scss'
|
||||||
@ -172,7 +172,7 @@ export const DisplayMeta = ({
|
|||||||
{(function () {
|
{(function () {
|
||||||
const profile = metadata[submittedBy]
|
const profile = metadata[submittedBy]
|
||||||
return (
|
return (
|
||||||
<UserComponent
|
<UserAvatar
|
||||||
pubkey={submittedBy}
|
pubkey={submittedBy}
|
||||||
name={
|
name={
|
||||||
profile?.display_name ||
|
profile?.display_name ||
|
||||||
@ -372,7 +372,7 @@ const DisplayUser = ({
|
|||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className={styles.tableCell}>
|
<TableCell className={styles.tableCell}>
|
||||||
<UserComponent
|
<UserAvatar
|
||||||
pubkey={user.pubkey}
|
pubkey={user.pubkey}
|
||||||
name={
|
name={
|
||||||
userMeta?.display_name ||
|
userMeta?.display_name ||
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
@ -14,7 +14,7 @@ import { Event, kinds, verifyEvent } from 'nostr-tools'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserComponent } from '../../components/username'
|
import { UserAvatar } from '../../components/UserAvatar'
|
||||||
import { MetadataController } from '../../controllers'
|
import { MetadataController } from '../../controllers'
|
||||||
import {
|
import {
|
||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
@ -36,6 +36,7 @@ import styles from './style.module.scss'
|
|||||||
import { Cancel, CheckCircle } from '@mui/icons-material'
|
import { Cancel, CheckCircle } from '@mui/icons-material'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { Container } from '../../components/Container'
|
||||||
|
|
||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -402,7 +403,7 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserComponent
|
<UserAvatar
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
name={
|
name={
|
||||||
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
|
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
|
||||||
@ -456,7 +457,7 @@ export const VerifyPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
<Box className={styles.container}>
|
<Container className={styles.container}>
|
||||||
{!meta && (
|
{!meta && (
|
||||||
<>
|
<>
|
||||||
<Typography component="label" variant="h6">
|
<Typography component="label" variant="h6">
|
||||||
@ -622,7 +623,7 @@ export const VerifyPage = () => {
|
|||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { Modal } from '../layouts/modal'
|
||||||
import { CreatePage } from '../pages/create'
|
import { CreatePage } from '../pages/create'
|
||||||
import { HomePage } from '../pages/home'
|
import { HomePage } from '../pages/home'
|
||||||
import { LandingPage } from '../pages/landing/LandingPage'
|
import { LandingPage } from '../pages/landing'
|
||||||
import { Login } from '../pages/login'
|
import { Login } from '../pages/login'
|
||||||
|
import { Nostr } from '../pages/nostr'
|
||||||
import { ProfilePage } from '../pages/profile'
|
import { ProfilePage } from '../pages/profile'
|
||||||
|
import { Register } from '../pages/register'
|
||||||
import { SettingsPage } from '../pages/settings/Settings'
|
import { SettingsPage } from '../pages/settings/Settings'
|
||||||
import { CacheSettingsPage } from '../pages/settings/cache'
|
import { CacheSettingsPage } from '../pages/settings/cache'
|
||||||
import { ProfileSettingsPage } from '../pages/settings/profile'
|
import { ProfileSettingsPage } from '../pages/settings/profile'
|
||||||
@ -10,6 +13,7 @@ import { RelaysPage } from '../pages/settings/relays'
|
|||||||
import { SignPage } from '../pages/sign'
|
import { SignPage } from '../pages/sign'
|
||||||
import { VerifyPage } from '../pages/verify'
|
import { VerifyPage } from '../pages/verify'
|
||||||
import { hexToNpub } from '../utils'
|
import { hexToNpub } from '../utils'
|
||||||
|
import { Route, RouteProps } from 'react-router-dom'
|
||||||
|
|
||||||
export const appPrivateRoutes = {
|
export const appPrivateRoutes = {
|
||||||
homePage: '/',
|
homePage: '/',
|
||||||
@ -25,8 +29,11 @@ export const appPublicRoutes = {
|
|||||||
profile: '/profile/:npub',
|
profile: '/profile/:npub',
|
||||||
landingPage: '/',
|
landingPage: '/',
|
||||||
login: '/login',
|
login: '/login',
|
||||||
|
register: '/login/register',
|
||||||
|
nostr: '/login/nostr',
|
||||||
verify: '/verify',
|
verify: '/verify',
|
||||||
source: 'https://git.sigit.io/sig/it'
|
source: 'https://git.sigit.io/sig/it',
|
||||||
|
docs: 'https://docs.sigit.io'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getProfileRoute = (hexKey: string) =>
|
export const getProfileRoute = (hexKey: string) =>
|
||||||
@ -35,16 +42,72 @@ export const getProfileRoute = (hexKey: string) =>
|
|||||||
export const getProfileSettingsRoute = (hexKey: string) =>
|
export const getProfileSettingsRoute = (hexKey: string) =>
|
||||||
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
|
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
|
||||||
|
|
||||||
export const publicRoutes = [
|
/**
|
||||||
|
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
||||||
|
*/
|
||||||
|
type CustomRouteProps<T> = T &
|
||||||
|
Omit<RouteProps, 'children'> & {
|
||||||
|
children?: Array<CustomRouteProps<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function maps over nested routes with optional condition for rendering
|
||||||
|
* @param {CustomRouteProps<T>[]} routes - routes list
|
||||||
|
* @param {RenderConditionCallbackFn} renderConditionCallbackFn - render condition callback (default true)
|
||||||
|
*/
|
||||||
|
export function recursiveRouteRenderer<T>(
|
||||||
|
routes?: CustomRouteProps<T>[],
|
||||||
|
renderConditionCallbackFn: (route: CustomRouteProps<T>) => boolean = () =>
|
||||||
|
true
|
||||||
|
) {
|
||||||
|
if (!routes) return null
|
||||||
|
|
||||||
|
// Callback allows us to pass arbitrary conditions for each route's rendering
|
||||||
|
// Skipping the callback will by default evaluate to true (show route)
|
||||||
|
return routes.map((route, index) =>
|
||||||
|
renderConditionCallbackFn(route) ? (
|
||||||
|
<Route
|
||||||
|
key={`${route.path}${index}`}
|
||||||
|
path={route.path}
|
||||||
|
element={route.element}
|
||||||
|
>
|
||||||
|
{recursiveRouteRenderer(route.children, renderConditionCallbackFn)}
|
||||||
|
</Route>
|
||||||
|
) : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicRouteProps = CustomRouteProps<{
|
||||||
|
hiddenWhenLoggedIn?: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const publicRoutes: PublicRouteProps[] = [
|
||||||
{
|
{
|
||||||
path: appPublicRoutes.landingPage,
|
path: appPublicRoutes.landingPage,
|
||||||
hiddenWhenLoggedIn: true,
|
hiddenWhenLoggedIn: true,
|
||||||
element: <LandingPage />
|
element: <LandingPage />,
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: appPublicRoutes.login,
|
element: <Modal />,
|
||||||
hiddenWhenLoggedIn: true,
|
children: [
|
||||||
element: <Login />
|
{
|
||||||
|
path: appPublicRoutes.login,
|
||||||
|
hiddenWhenLoggedIn: true,
|
||||||
|
element: <Login />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.register,
|
||||||
|
hiddenWhenLoggedIn: true,
|
||||||
|
element: <Register />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.nostr,
|
||||||
|
hiddenWhenLoggedIn: true,
|
||||||
|
element: <Nostr />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: appPublicRoutes.profile,
|
path: appPublicRoutes.profile,
|
||||||
|
17
src/styles/colors.scss
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
$primary-main: #4c82a3;
|
||||||
|
$primary-light: #5e8eab;
|
||||||
|
$primary-dark: #447592;
|
||||||
|
|
||||||
|
$secondary-main: #7d54a3;
|
||||||
|
|
||||||
|
$box-shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
$border-color: #27323c;
|
||||||
|
|
||||||
|
$body-background-color: #ededed;
|
||||||
|
$overlay-background-color: #ffffff;
|
||||||
|
|
||||||
|
$text-color: #434343;
|
||||||
|
$input-text-color: #717171;
|
||||||
|
|
||||||
|
$relay-status-connected: #178b13;
|
||||||
|
$relay-status-notconnected: #d82222;
|
3
src/styles/form.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.label {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
4
src/styles/sizes.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
$header-height: 65px;
|
||||||
|
$body-vertical-padding: 25px;
|
||||||
|
|
||||||
|
$default-container-padding-inline: 10px;
|
7
src/styles/typography.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
$font-familiy: 'Roboto', system-ui, sans-serif;
|
||||||
|
|
||||||
|
$letter-spacing: 1px;
|
||||||
|
|
||||||
|
$body-font-size: 18px;
|
||||||
|
$body-line-height: 1.5;
|
||||||
|
$body-font-weight: 500;
|
@ -8,7 +8,9 @@ export const theme = extendTheme({
|
|||||||
light: {
|
light: {
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#291334'
|
main: '#4c82a3',
|
||||||
|
light: '#5e8eab',
|
||||||
|
dark: '447592'
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: '#3abff8'
|
main: '#3abff8'
|
||||||
@ -21,14 +23,41 @@ export const theme = extendTheme({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
MuiModal: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
insetBlock: '25px',
|
||||||
|
insetInline: '10px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
borderRadius: '2rem'
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '8px 15px',
|
||||||
|
transition: 'ease 0.2s',
|
||||||
|
boxShadow: 'unset',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
borderRadius: '4px',
|
||||||
|
':hover': {
|
||||||
|
background: 'var(--primary-light)',
|
||||||
|
boxShadow: 'unset'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: 'inherit',
|
||||||
|
background: 'transparent',
|
||||||
|
':hover': {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
contained: {
|
contained: {
|
||||||
|
background: 'var(--primary-main)',
|
||||||
|
color: 'white',
|
||||||
':hover': {
|
':hover': {
|
||||||
background: '#150a1a'
|
color: 'white'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
outlined: {
|
outlined: {
|
||||||
@ -37,8 +66,26 @@ export const theme = extendTheme({
|
|||||||
borderColor: '#291334',
|
borderColor: '#291334',
|
||||||
background: '#29133433'
|
background: '#29133433'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
startIcon: {
|
||||||
|
marginRight: '12px',
|
||||||
|
marginLeft: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
MuiTypography: {
|
||||||
|
defaultProps: {
|
||||||
|
fontFamily: ['Roboto', 'system-ui', 'sans-serif'].join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
breakpoints: {
|
||||||
|
values: {
|
||||||
|
xs: 0,
|
||||||
|
sm: 600,
|
||||||
|
md: 900,
|
||||||
|
lg: 1200,
|
||||||
|
xl: 1420
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|