Compare commits

...

54 Commits

Author SHA1 Message Date
97aa87d8b1 Merge branch 'staging' into issue-99 2024-08-05 11:56:38 +00:00
e280e87342 fix: footer buttons
All checks were successful
Release to Staging / build_and_release (push) Successful in 57s
center text within buttons, buttons container centered, add links to docs and source
2024-08-02 17:47:21 +02:00
c29bdd59ae Merge pull request 'Improve font support, fix svg inline' (#123) from issue-fonts-support into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m3s
Reviewed-on: #123
Reviewed-by: Otto <y@noreply.git.nostrdev.com>
2024-08-02 09:52:14 +00:00
0a74ad97b2 fix: disable login, register fields, add coming soon
Login and register fields, and buttons are not interactive. Pages show Coming soon!
2024-07-31 18:35:45 +02:00
21bfda19f9 refactor: remove unused fonts, reduce number of variations 2024-07-31 18:13:18 +02:00
a63ea913d9 fix: improve font support
Add local variations and web (eot, woff2, woff, ttf).
2024-07-31 17:41:40 +02:00
c22b1e4b5a fix: inlined svg background images
surround imported svg with double quotes, vite recommendation
2024-07-31 17:31:23 +02:00
3470bf78b7 refactor: nostr gif, use it from assets instead link 2024-07-31 17:29:34 +02:00
b
5445120511 Merge pull request 'Landing page - new design implementation' (#122) from issue-21 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m2s
Reviewed-on: #122
Reviewed-by: s <sabir@4gl.io>
2024-07-31 13:06:57 +00:00
6f4737d75c fix: icons, use FontAwesome package 2024-07-31 14:37:10 +02:00
20bb05ddc6 feat: add UserAvatar, UserIconButton
Closes #68 - Only use getRoboHashPicture function (set 1) for Avatars
2024-07-31 13:53:36 +02:00
5d415a2359 refactor: recursiveRouteRenderer params update and comments 2024-07-31 13:48:57 +02:00
1070c9a8f9 refactor: review colors, usage
Requested Change
2024-07-31 13:39:16 +02:00
fcd00d9e9c fix: font url typo 2024-07-31 13:36:41 +02:00
dc11f5695d refactor: update main layout loading desc 2024-07-30 16:25:36 +02:00
d7f9807e20 fix: color scheme 2024-07-30 16:16:52 +02:00
5a4da1834b fix: loading spinner, improve desc readability, use favicon instead of circle 2024-07-30 13:29:03 +02:00
202c98c94c fix: background overlap 2024-07-30 13:23:06 +02:00
e9a1b9894c feat: add background images 2024-07-30 13:14:18 +02:00
f1a26e4dc4 chore: styling cleanup 2024-07-30 12:23:38 +02:00
aa5aa60c6a fix: fonts 2024-07-30 12:00:57 +02:00
3a93622966 fix: svg attributes 2024-07-30 10:38:06 +02:00
c5b1a9b380 fix: title text align 2024-07-30 10:35:09 +02:00
53b7b05ac5 fix: top level container wrapper for other pages 2024-07-30 10:28:57 +02:00
baa1a7b040 fix: works offline card icon 2024-07-30 10:25:57 +02:00
0d49c49459 fix: card icons 2024-07-30 09:34:52 +02:00
99856fd8f2 fix: gap, spacing 2024-07-29 18:55:19 +02:00
d0a6297cce fix: remove placeholder used for text 2024-07-29 18:25:32 +02:00
9dae3a48be fix: IconButton conflict, username layout 2024-07-29 18:06:23 +02:00
e3ca3ab908 fix: popup forms designs 2024-07-29 17:12:47 +02:00
c7dfb2864a fix: list item key 2024-07-29 15:28:19 +02:00
55158fc313 fix: update popup design 2024-07-29 15:27:20 +02:00
28184ab038 fix: update buttons and button icon design 2024-07-29 14:39:30 +02:00
af689a00f7 fix: update footer design 2024-07-29 14:12:01 +02:00
5d59ffce28 fix: update design buttons 2024-07-29 14:03:16 +02:00
9189ff33bc fix: reduce mui usage, implement design updates 2024-07-29 11:22:04 +02:00
e54eced800 feat: add custom Container component for layouts 2024-07-29 11:20:39 +02:00
2cd851a7c1 fix: add default typography styles 2024-07-29 11:19:41 +02:00
6a1f04ec6b fix: add Roboto font 2024-07-29 11:16:27 +02:00
b5de0ce04a chore: remove unused css and fonts 2024-07-29 11:15:59 +02:00
804bb6c9ac fix: composition for links and buttons 2024-07-26 16:03:45 +02:00
3c22429941 fix: move nostr login to nostr route 2024-07-26 15:45:13 +02:00
868ae6f23e feat: add modal with login, register, nostr routes 2024-07-26 15:38:44 +02:00
87c6807ba0 fix: app bar z-index 2024-07-26 15:37:40 +02:00
06deecba76 chore: landing page comments cleanup 2024-07-26 13:18:05 +02:00
0b35f11abf feat: add children support to routes arrays 2024-07-26 13:15:14 +02:00
017d1ab88b fix: update logo and favicon 2024-07-26 10:43:19 +02:00
8fa074899c chore: move scss styles to separate folder 2024-07-26 10:42:15 +02:00
45f0764fa8 fix: footer padding and responsiveness 2024-07-25 18:03:03 +02:00
5b9093a6e7 chore: remove unused css 2024-07-25 16:46:52 +02:00
87e4536713 feat: landing page - responsive cards 2024-07-25 16:29:18 +02:00
3149ba9757 feat: landing page - larger cta button 2024-07-25 16:11:20 +02:00
a82f138057 chore: remove unused css file 2024-07-25 15:06:55 +02:00
0a61ae5f64 feat: landing page implementation and styling 2024-07-25 15:05:47 +02:00
80 changed files with 2076 additions and 1080 deletions

62
package-lock.json generated
View File

@ -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",

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

34
public/logo.svg Normal file
View 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>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -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
View 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;
}
}

View File

@ -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>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

View 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>

After

Width:  |  Height:  |  Size: 364 B

View 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>

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

BIN
src/assets/images/nostr.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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;

View File

@ -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>
) )
} }

View File

@ -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;

View 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>
}

View File

@ -0,0 +1,5 @@
.icon {
border-radius: 100px;
width: 20px;
height: 20px;
}

View 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>
)
}

View File

@ -0,0 +1,8 @@
@import '../../styles/sizes.scss';
.container {
width: 100%;
max-width: 1400px;
padding-inline: $default-container-padding-inline;
margin-inline: auto;
}

View 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>
}

View File

@ -0,0 +1,7 @@
.iconStackContainer {
position: relative;
> * {
position: absolute;
}
}

View 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&nbsp;
<a href="https://nostrdev.com/" target="_blank">
Nostr Dev
</a>{' '}
2024.
</div>
</footer>
)

View 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;
}
}

View 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>
)
}

View 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;
}

View File

@ -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>
) )

View File

@ -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);

View 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>
)
}

View 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);
}

View 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>
)
}

View File

@ -0,0 +1,7 @@
.icon {
width: 40px;
height: 40px;
border-radius: 50%;
border-width: 3px;
overflow: hidden;
}

View File

@ -0,0 +1,7 @@
.container {
display: flex;
flex-direction: row;
justify-content: end;
align-items: center;
gap: 12px;
}

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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
View 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>
)
}

View 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;
}

View File

@ -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');
} }

View File

@ -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 ||

View File

@ -1,4 +1,4 @@
@import '../../colors.scss'; @import '../../styles/colors.scss';
.container { .container {
display: flex; display: flex;

View File

@ -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 ||

View File

@ -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);
} }
} }
} }

View File

@ -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
View 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 &amp; 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 &amp; 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 &amp; 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>
)
}

View File

@ -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;
} }
} }

View File

@ -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>
</> </>
) )
} }

View File

@ -1,6 +0,0 @@
@import '../../colors.scss';
.loginPage {
color: $text-color;
margin-top: 20px;
}

400
src/pages/nostr/index.tsx Normal file
View 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>
</>
)
}

View File

@ -0,0 +1 @@
@import '../../styles/form.scss';

View File

@ -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>
)} )}
</> </>
) )

View 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>
</>
)
}

View File

@ -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>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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>
</> </>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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 {

View File

@ -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>
</> </>
) )
} }

View File

@ -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 ||

View File

@ -1,4 +1,4 @@
@import '../../colors.scss'; @import '../../styles/colors.scss';
.container { .container {
color: $text-color; color: $text-color;

View File

@ -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>
</> </>
) )
} }

View File

@ -1,4 +1,4 @@
@import '../../colors.scss'; @import '../../styles/colors.scss';
.container { .container {
color: $text-color; color: $text-color;

View File

@ -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
View 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
View File

@ -0,0 +1,3 @@
.label {
font-size: 16px;
}

4
src/styles/sizes.scss Normal file
View File

@ -0,0 +1,4 @@
$header-height: 65px;
$body-vertical-padding: 25px;
$default-container-padding-inline: 10px;

View 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;

View File

@ -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
} }
} }
}) })