tangled
alpha
login
or
join now
cosmik.network
/
semble
43
fork
atom
A social knowledge tool for researchers built on ATProto
43
fork
atom
overview
issues
13
pulls
pipelines
feat: basic app shell
Pouria Delfanazari
7 months ago
b8afa1d6
544119f0
+247
-88
11 changed files
expand all
collapse all
unified
split
src
webapp
app
(authenticated)
layout.tsx
layout.tsx
components
navigation
header
Header.tsx
navItem
NavItem.tsx
navbar
Navbar.tsx
package-lock.json
package.json
providers
index.tsx
mantine.tsx
tanstack.tsx
styles
theme.tsx
+16
-76
src/webapp/app/(authenticated)/layout.tsx
···
1
1
'use client';
2
2
3
3
import { useEffect } from 'react';
4
4
-
import { usePathname, useRouter } from 'next/navigation';
4
4
+
import { useRouter } from 'next/navigation';
5
5
import { useAuth } from '@/hooks/useAuth';
6
6
-
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
7
7
-
import {
8
8
-
ActionIcon,
9
9
-
AppShell,
10
10
-
Group,
11
11
-
NavLink,
12
12
-
Text,
13
13
-
Affix,
14
14
-
} from '@mantine/core';
15
15
-
import { FiSidebar } from 'react-icons/fi';
16
16
-
import { IoDocumentTextOutline } from 'react-icons/io5';
17
17
-
import { BsFolder2 } from 'react-icons/bs';
18
18
-
import { BiUser } from 'react-icons/bi';
6
6
+
import { useDisclosure } from '@mantine/hooks';
7
7
+
import { ActionIcon, AppShell, Affix } from '@mantine/core';
19
8
import { FiPlus } from 'react-icons/fi';
20
20
-
import { HiOutlineGlobeAlt } from 'react-icons/hi';
9
9
+
import Header from '@/components/navigation/header/Header';
10
10
+
import Navbar from '@/components/navigation/navbar/Navbar';
21
11
22
22
-
export default function AuthenticatedLayout({
23
23
-
children,
24
24
-
}: {
12
12
+
interface Props {
25
13
children: React.ReactNode;
26
26
-
}) {
27
27
-
const { isAuthenticated, isLoading } = useAuth();
14
14
+
}
15
15
+
export default function AuthenticatedLayout(props: Props) {
28
16
const router = useRouter();
29
29
-
30
30
-
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
31
31
-
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
32
32
-
const isMobile = useMediaQuery('(max-width: 768px)');
33
33
-
34
34
-
const pathname = usePathname();
17
17
+
const { isAuthenticated, isLoading } = useAuth();
18
18
+
const [opened, { toggle }] = useDisclosure();
35
19
36
20
useEffect(() => {
37
21
if (!isLoading && !isAuthenticated) {
···
49
33
navbar={{
50
34
width: 300,
51
35
breakpoint: 'sm',
52
52
-
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
36
36
+
collapsed: { mobile: !opened, desktop: opened },
53
37
}}
54
38
padding="md"
55
39
>
56
56
-
<AppShell.Header>
57
57
-
<Group h="100%" px="md" gap={'xs'}>
58
58
-
<ActionIcon
59
59
-
variant="subtle"
60
60
-
size="lg"
61
61
-
onClick={() => {
62
62
-
isMobile ? toggleMobile() : toggleDesktop();
63
63
-
}}
64
64
-
>
65
65
-
<FiSidebar />
66
66
-
</ActionIcon>
67
67
-
<Text fw={600}>Semble</Text>
68
68
-
</Group>
69
69
-
</AppShell.Header>
70
70
-
71
71
-
<AppShell.Navbar p="md">
72
72
-
<NavLink
73
73
-
href="/explore"
74
74
-
label="Explore"
75
75
-
active={pathname === '/explore'}
76
76
-
leftSection={<HiOutlineGlobeAlt />}
77
77
-
/>
78
78
-
<NavLink
79
79
-
href="/library"
80
80
-
label="My cards"
81
81
-
active={pathname === '/library'}
82
82
-
leftSection={<IoDocumentTextOutline />}
83
83
-
/>
84
84
-
<NavLink
85
85
-
href="/collections"
86
86
-
label="My collections"
87
87
-
active={pathname === '/collections'}
88
88
-
leftSection={<BsFolder2 />}
89
89
-
/>
90
90
-
<NavLink
91
91
-
href="/profile"
92
92
-
label="Profile"
93
93
-
active={pathname === '/profile'}
94
94
-
leftSection={<BiUser />}
95
95
-
mt="auto"
96
96
-
/>
97
97
-
</AppShell.Navbar>
40
40
+
<Header onToggleNavbar={toggle} />
41
41
+
<Navbar />
98
42
99
43
<AppShell.Main>
100
100
-
{children}
44
44
+
{props.children}
101
45
<Affix position={{ bottom: 20, right: 20 }}>
102
46
<ActionIcon
103
47
onClick={() => router.push('/cards/add')}
104
104
-
size={56}
48
48
+
size={'input-lg'}
105
49
radius="xl"
106
106
-
color="blue"
107
50
variant="filled"
108
108
-
style={{
109
109
-
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
110
110
-
}}
111
51
>
112
112
-
<FiPlus size={24} />
52
52
+
<FiPlus size={26} />
113
53
</ActionIcon>
114
54
</Affix>
115
55
</AppShell.Main>
+3
-11
src/webapp/app/layout.tsx
···
1
1
import { Analytics } from '@vercel/analytics/next';
2
2
import type { Metadata } from 'next';
3
3
-
import {
4
4
-
ColorSchemeScript,
5
5
-
mantineHtmlProps,
6
6
-
MantineProvider,
7
7
-
} from '@mantine/core';
8
8
-
import '@mantine/core/styles.css';
9
9
-
import { AuthProvider } from '@/hooks/useAuth';
10
10
-
import { theme } from '@/styles/theme';
3
3
+
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
11
4
import { Hanken_Grotesk } from 'next/font/google';
5
5
+
import Providers from '@/providers';
12
6
13
7
export const metadata: Metadata = {
14
8
title: {
···
37
31
<ColorSchemeScript />
38
32
</head>
39
33
<body>
40
40
-
<MantineProvider theme={theme}>
41
41
-
<AuthProvider>{children}</AuthProvider>
42
42
-
</MantineProvider>
34
34
+
<Providers>{children}</Providers>
43
35
<Analytics />
44
36
</body>
45
37
</html>
+34
src/webapp/components/navigation/header/Header.tsx
···
1
1
+
import { ActionIcon, AppShellHeader, Group, Image } from '@mantine/core';
2
2
+
import SembleLogo from '@/assets/semble-logo.svg';
3
3
+
import { FiSidebar } from 'react-icons/fi';
4
4
+
5
5
+
interface Props {
6
6
+
onToggleNavbar: () => void;
7
7
+
}
8
8
+
9
9
+
export default function Header(props: Props) {
10
10
+
return (
11
11
+
<AppShellHeader withBorder={false}>
12
12
+
<Group h="100%" px="md" gap={'xs'} justify="space-between">
13
13
+
<Group>
14
14
+
<Image
15
15
+
src={SembleLogo.src}
16
16
+
alt="Semble logo"
17
17
+
w={'auto'}
18
18
+
h={28}
19
19
+
ml={'xs'}
20
20
+
/>
21
21
+
<ActionIcon
22
22
+
variant="subtle"
23
23
+
color="gray"
24
24
+
size={'lg'}
25
25
+
radius={'xl'}
26
26
+
onClick={props.onToggleNavbar}
27
27
+
>
28
28
+
<FiSidebar size={22} />
29
29
+
</ActionIcon>
30
30
+
</Group>
31
31
+
</Group>
32
32
+
</AppShellHeader>
33
33
+
);
34
34
+
}
+31
src/webapp/components/navigation/navItem/NavItem.tsx
···
1
1
+
'use client';
2
2
+
3
3
+
import { NavLink } from '@mantine/core';
4
4
+
import Link from 'next/link';
5
5
+
import { usePathname } from 'next/navigation';
6
6
+
7
7
+
interface Props {
8
8
+
href: string;
9
9
+
label: string;
10
10
+
icon: React.ReactElement;
11
11
+
activeIcon: React.ReactElement;
12
12
+
badge?: number;
13
13
+
}
14
14
+
15
15
+
export default function NavItem(props: Props) {
16
16
+
const pathname = usePathname();
17
17
+
const isActive = pathname === props.href;
18
18
+
19
19
+
return (
20
20
+
<NavLink
21
21
+
component={Link}
22
22
+
href={props.href}
23
23
+
variant="subtle"
24
24
+
c="gray"
25
25
+
px={'6'}
26
26
+
fw={600}
27
27
+
label={props.label}
28
28
+
leftSection={isActive ? props.activeIcon : props.icon}
29
29
+
/>
30
30
+
);
31
31
+
}
+43
src/webapp/components/navigation/navbar/Navbar.tsx
···
1
1
+
import NavItem from '../navItem/NavItem';
2
2
+
import {
3
3
+
AppShellSection,
4
4
+
AppShellNavbar,
5
5
+
ScrollArea,
6
6
+
Divider,
7
7
+
} from '@mantine/core';
8
8
+
import { IoDocumentTextOutline } from 'react-icons/io5';
9
9
+
import { MdOutlineEmojiNature } from 'react-icons/md';
10
10
+
11
11
+
export default function Navbar() {
12
12
+
return (
13
13
+
<AppShellNavbar withBorder={false}>
14
14
+
<AppShellSection
15
15
+
grow
16
16
+
component={ScrollArea}
17
17
+
px={'md'}
18
18
+
pb={'md'}
19
19
+
pt={'xs'}
20
20
+
>
21
21
+
<NavItem
22
22
+
href="/library"
23
23
+
label="Library"
24
24
+
icon={<IoDocumentTextOutline size={25} />}
25
25
+
activeIcon={<IoDocumentTextOutline size={25} />}
26
26
+
/>
27
27
+
<NavItem
28
28
+
href="/explore"
29
29
+
label="Explore"
30
30
+
icon={<MdOutlineEmojiNature size={25} />}
31
31
+
activeIcon={<MdOutlineEmojiNature size={25} />}
32
32
+
/>
33
33
+
34
34
+
<Divider my={'sm'} />
35
35
+
{/*<FeedNavList />
36
36
+
<ListNavList />
37
37
+
<ChatNavList />*/}
38
38
+
</AppShellSection>
39
39
+
40
40
+
<AppShellSection p={'md'}></AppShellSection>
41
41
+
</AppShellNavbar>
42
42
+
);
43
43
+
}
+25
src/webapp/package-lock.json
···
14
14
"@mantine/form": "^8.1.3",
15
15
"@mantine/hooks": "^8.1.3",
16
16
"@mantine/notifications": "^8.1.3",
17
17
+
"@tanstack/react-query": "^5.85.5",
17
18
"@vercel/analytics": "^1.5.0",
18
19
"date-fns": "^4.1.0",
19
20
"dayjs": "^1.11.13",
···
7073
7074
},
7074
7075
"engines": {
7075
7076
"node": ">=14.16"
7077
7077
+
}
7078
7078
+
},
7079
7079
+
"node_modules/@tanstack/query-core": {
7080
7080
+
"version": "5.85.5",
7081
7081
+
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz",
7082
7082
+
"integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==",
7083
7083
+
"funding": {
7084
7084
+
"type": "github",
7085
7085
+
"url": "https://github.com/sponsors/tannerlinsley"
7086
7086
+
}
7087
7087
+
},
7088
7088
+
"node_modules/@tanstack/react-query": {
7089
7089
+
"version": "5.85.5",
7090
7090
+
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz",
7091
7091
+
"integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==",
7092
7092
+
"dependencies": {
7093
7093
+
"@tanstack/query-core": "5.85.5"
7094
7094
+
},
7095
7095
+
"funding": {
7096
7096
+
"type": "github",
7097
7097
+
"url": "https://github.com/sponsors/tannerlinsley"
7098
7098
+
},
7099
7099
+
"peerDependencies": {
7100
7100
+
"react": "^18 || ^19"
7076
7101
}
7077
7102
},
7078
7103
"node_modules/@testing-library/dom": {
+1
src/webapp/package.json
···
30
30
"@mantine/form": "^8.1.3",
31
31
"@mantine/hooks": "^8.1.3",
32
32
"@mantine/notifications": "^8.1.3",
33
33
+
"@tanstack/react-query": "^5.85.5",
33
34
"@vercel/analytics": "^1.5.0",
34
35
"date-fns": "^4.1.0",
35
36
"dayjs": "^1.11.13",
+19
src/webapp/providers/index.tsx
···
1
1
+
'use client';
2
2
+
3
3
+
import { AuthProvider } from '@/hooks/useAuth';
4
4
+
import MantineProvider from './mantine';
5
5
+
import TanStackQueryProvider from './tanstack';
6
6
+
7
7
+
interface Props {
8
8
+
children: React.ReactNode;
9
9
+
}
10
10
+
11
11
+
export default function Providers(props: Props) {
12
12
+
return (
13
13
+
<TanStackQueryProvider>
14
14
+
<AuthProvider>
15
15
+
<MantineProvider>{props.children}</MantineProvider>
16
16
+
</AuthProvider>
17
17
+
</TanStackQueryProvider>
18
18
+
);
19
19
+
}
+13
src/webapp/providers/mantine.tsx
···
1
1
+
'use client';
2
2
+
3
3
+
import { theme } from '@/styles/theme';
4
4
+
import { MantineProvider as BaseProvider } from '@mantine/core';
5
5
+
import '@mantine/core/styles.css';
6
6
+
7
7
+
interface Props {
8
8
+
children: React.ReactNode;
9
9
+
}
10
10
+
11
11
+
export default function MantineProvider(props: Props) {
12
12
+
return <BaseProvider theme={theme}>{props.children}</BaseProvider>;
13
13
+
}
+53
src/webapp/providers/tanstack.tsx
···
1
1
+
'use client';
2
2
+
3
3
+
import {
4
4
+
QueryClient,
5
5
+
QueryClientProvider,
6
6
+
defaultShouldDehydrateQuery,
7
7
+
isServer,
8
8
+
} from '@tanstack/react-query';
9
9
+
10
10
+
function makeQueryClient() {
11
11
+
return new QueryClient({
12
12
+
defaultOptions: {
13
13
+
queries: {
14
14
+
staleTime: 60 * 1000,
15
15
+
},
16
16
+
dehydrate: {
17
17
+
// include pending queries in dehydration
18
18
+
shouldDehydrateQuery: (query) =>
19
19
+
defaultShouldDehydrateQuery(query) ||
20
20
+
query.state.status === 'pending',
21
21
+
},
22
22
+
},
23
23
+
});
24
24
+
}
25
25
+
26
26
+
let browserQueryClient: QueryClient | undefined = undefined;
27
27
+
28
28
+
function getQueryClient() {
29
29
+
if (isServer) {
30
30
+
// Server: always make a new query client
31
31
+
return makeQueryClient();
32
32
+
} else {
33
33
+
// Browser: make a new query client if we don't already have one
34
34
+
// This is very important, so we don't re-make a new client if React
35
35
+
// suspends during the initial render. This may not be needed if we
36
36
+
// have a suspense boundary BELOW the creation of the query client
37
37
+
if (!browserQueryClient) browserQueryClient = makeQueryClient();
38
38
+
return browserQueryClient;
39
39
+
}
40
40
+
}
41
41
+
42
42
+
interface Props {
43
43
+
children: React.ReactNode;
44
44
+
}
45
45
+
export default function TanStackQueryProvider(props: Props) {
46
46
+
const queryClient = getQueryClient();
47
47
+
48
48
+
return (
49
49
+
<QueryClientProvider client={queryClient}>
50
50
+
{props.children}
51
51
+
</QueryClientProvider>
52
52
+
);
53
53
+
}
+9
-1
src/webapp/styles/theme.tsx
···
1
1
'use client';
2
2
3
3
-
import { Button, createTheme, TextInput } from '@mantine/core';
3
3
+
import { Button, createTheme, NavLink, TextInput } from '@mantine/core';
4
4
5
5
export const theme = createTheme({
6
6
primaryColor: 'tangerine',
···
50
50
defaultProps: {
51
51
radius: 'xl',
52
52
},
53
53
+
}),
54
54
+
NavLink: NavLink.extend({
55
55
+
styles: (theme) => ({
56
56
+
label: {
57
57
+
fontSize: theme.fontSizes.md,
58
58
+
fontWeight: 600,
59
59
+
},
60
60
+
}),
53
61
}),
54
62
},
55
63
});