This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: sign up and log in page designs

+863 -476
+2 -2
src/webapp/app/auth/complete/page.tsx
··· 75 75 return ( 76 76 <Stack align="center"> 77 77 <Stack gap={0} align="center"> 78 - <Title order={2}>Completing Sign In</Title> 78 + <Title order={2}>Signing you in...</Title> 79 79 <Text>{message}</Text> 80 80 </Stack> 81 - <Loader type="bars" /> 81 + <Loader type="dots" /> 82 82 </Stack> 83 83 ); 84 84 }
src/webapp/app/favicon.ico

This is a binary file and will not be displayed.

+19
src/webapp/app/login/layout.tsx
··· 1 + import { Center, Container } from '@mantine/core'; 2 + import { Metadata } from 'next'; 3 + 4 + export const metadata: Metadata = { 5 + title: 'Log in', 6 + description: 'Welcome back', 7 + }; 8 + 9 + interface Props { 10 + children: React.ReactNode; 11 + } 12 + 13 + export default function Layout(props: Props) { 14 + return ( 15 + <Container p={'sm'}> 16 + <Center h={'100svh'}>{props.children}</Center> 17 + </Container> 18 + ); 19 + }
+87 -236
src/webapp/app/login/page.tsx
··· 1 1 'use client'; 2 2 3 - import { Suspense } from 'react'; 4 - import { useState, useEffect } from 'react'; 5 - import { useRouter, useSearchParams } from 'next/navigation'; 6 - import { useAuth } from '@/hooks/useAuth'; 7 - import { ApiClient } from '@/api-client/ApiClient'; 8 - import { ExtensionService } from '@/services/extensionService'; 3 + import LoginForm from '@/features/auth/components/loginForm/LoginForm'; 9 4 import { 5 + Stack, 10 6 Title, 7 + Text, 8 + Image, 9 + Anchor, 10 + Popover, 11 11 Button, 12 - Stack, 13 - Center, 14 - Card, 15 - TextInput, 16 - PasswordInput, 17 - Text, 18 - Group, 12 + PopoverTarget, 13 + PopoverDropdown, 19 14 Loader, 20 15 } from '@mantine/core'; 21 - import { getAccessToken } from '@/services/auth'; 16 + import { Suspense, useEffect, useState } from 'react'; 17 + import { IoMdHelpCircleOutline } from 'react-icons/io'; 18 + import SembleLogo from '@/assets/semble-logo.svg'; 19 + import { useAuth } from '@/hooks/useAuth'; 20 + import { useRouter } from 'next/navigation'; 22 21 23 - function LoginForm() { 24 - const [handle, setHandle] = useState(''); 25 - const [appPassword, setAppPassword] = useState(''); 26 - const [isLoading, setIsLoading] = useState(false); 27 - const [error, setError] = useState(''); 28 - const [useAppPassword, setUseAppPassword] = useState(false); 29 - const [isCheckingAuth, setIsCheckingAuth] = useState(true); 22 + export default function Page() { 23 + const { isAuthenticated, isLoading } = useAuth(); 24 + const [isRedirecting, setIsRedirecting] = useState(false); 30 25 const router = useRouter(); 31 - const searchParams = useSearchParams(); 32 - const { setTokens, isAuthenticated } = useAuth(); 33 - 34 - const isExtensionLogin = searchParams.get('extension-login') === 'true'; 35 26 36 - // Create API client instance 37 - const apiClient = new ApiClient( 38 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000', 39 - () => getAccessToken(), // Use auth token when available 40 - ); 41 - 42 - // Handle extension login if user is already authenticated 43 27 useEffect(() => { 44 - if (isExtensionLogin && isAuthenticated) { 45 - handleExtensionTokenGeneration(); 46 - } else if (isAuthenticated && !isExtensionLogin) { 47 - // If user is already authenticated and not doing extension login, redirect to library 48 - router.push('/library'); 49 - } else { 50 - // Auth check is complete, show the login form 51 - setIsCheckingAuth(false); 52 - } 53 - }, [isExtensionLogin, isAuthenticated]); 54 - 55 - const handleExtensionTokenGeneration = async () => { 56 - try { 57 - setIsLoading(true); 58 - const tokens = await apiClient.generateExtensionTokens(); 59 - 60 - await ExtensionService.sendTokensToExtension(tokens); 61 - 62 - setError(''); 63 - 64 - // Clear the extension tokens requested flag 65 - ExtensionService.clearExtensionTokensRequested(); 66 - 67 - // Redirect to extension success page after successful extension token generation 68 - router.push('/extension/auth/complete'); 69 - } catch (err: any) { 70 - // Clear the flag even on failure 71 - ExtensionService.clearExtensionTokensRequested(); 72 - 73 - // Redirect to extension error page 74 - router.push('/extension/auth/error'); 75 - } finally { 76 - setIsLoading(false); 77 - } 78 - }; 28 + if (isAuthenticated) { 29 + setIsRedirecting(true); 79 30 80 - const handleOAuthSubmit = async (e: React.FormEvent) => { 81 - e.preventDefault(); 82 - 83 - if (!handle) { 84 - setError('Please enter your Bluesky handle'); 85 - return; 86 - } 87 - 88 - setIsLoading(true); 89 - setError(''); 90 - 91 - try { 92 - // If this is an extension login, persist the flag before redirect 93 - if (searchParams.get('extension-login') === 'true') { 94 - ExtensionService.setExtensionTokensRequested(); 95 - } 96 - 97 - const { authUrl } = await apiClient.initiateOAuthSignIn({ handle }); 98 - 99 - // Redirect to the auth URL from the API 100 - window.location.href = authUrl; 101 - } catch (err: any) { 102 - setError(err.message || 'An error occurred during login'); 103 - } finally { 104 - setIsLoading(false); 105 - } 106 - }; 107 - 108 - const handleAppPasswordSubmit = async (e: React.FormEvent) => { 109 - e.preventDefault(); 110 - 111 - if (!handle || !appPassword) { 112 - setError('Please enter both your handle and app password'); 113 - return; 114 - } 115 - 116 - setIsLoading(true); 117 - setError(''); 118 - 119 - try { 120 - const { accessToken, refreshToken } = 121 - await apiClient.loginWithAppPassword({ 122 - identifier: handle, 123 - appPassword: appPassword, 124 - }); 125 - 126 - // Set tokens 127 - setTokens(accessToken, refreshToken); 128 - 129 - // Handle extension login or redirect to dashboard 130 - if (isExtensionLogin) { 131 - await handleExtensionTokenGeneration(); 132 - } else { 31 + // redirect after 1 second 32 + const id = setTimeout(() => { 133 33 router.push('/library'); 134 - } 135 - } catch (err: any) { 136 - setError(err.message || 'Invalid credentials'); 137 - } finally { 138 - setIsLoading(false); 34 + }, 1000); 139 35 } 140 - }; 36 + }, [isAuthenticated, router]); 141 37 142 - // Show loading while checking authentication status 143 - if (isCheckingAuth || isAuthenticated) { 38 + if (isLoading) { 144 39 return ( 145 - <Center h={'100svh'}> 146 - <Stack align="center"> 147 - <Loader size="lg" /> 148 - </Stack> 149 - </Center> 40 + <Stack align="center"> 41 + <Loader type="dots" /> 42 + </Stack> 150 43 ); 151 44 } 152 45 153 - return ( 154 - <Center h={'100svh'}> 46 + if (isRedirecting) { 47 + return ( 155 48 <Stack align="center"> 156 - <Title order={1}> 157 - {isExtensionLogin ? 'Sign in for Extension' : 'Sign in with Bluesky'} 158 - </Title> 159 - 160 - <Card withBorder w={400}> 161 - {!useAppPassword ? ( 162 - <form onSubmit={handleOAuthSubmit}> 163 - <Stack> 164 - <Stack> 165 - <TextInput 166 - id="handle" 167 - label="Enter your Bluesky handle" 168 - placeholder="username.bsky.social" 169 - value={handle} 170 - onChange={(e) => setHandle(e.target.value)} 171 - /> 172 - {error && ( 173 - <Text fz={'sm'} c={'red'}> 174 - {error} 175 - </Text> 176 - )} 177 - </Stack> 178 - 179 - <Group grow> 180 - <Button type="submit" loading={isLoading}> 181 - {isLoading ? 'Connecting...' : 'Continue'} 182 - </Button> 183 - </Group> 184 - 185 - <Stack> 186 - <Button 187 - type="button" 188 - onClick={() => setUseAppPassword(true)} 189 - variant="transparent" 190 - color="blue" 191 - > 192 - Sign in with app password 193 - </Button> 194 - </Stack> 195 - </Stack> 196 - </form> 197 - ) : ( 198 - <form onSubmit={handleAppPasswordSubmit}> 199 - <Stack> 200 - <Stack> 201 - <TextInput 202 - id="handle-app" 203 - label="Bluesky handle" 204 - placeholder="username.bsky.social" 205 - value={handle} 206 - onChange={(e) => setHandle(e.target.value)} 207 - /> 208 - 209 - <Stack> 210 - <PasswordInput 211 - id="app-password" 212 - label="App password" 213 - placeholder="xxxx-xxxx-xxxx-xxxx" 214 - value={appPassword} 215 - onChange={(e) => setAppPassword(e.target.value)} 216 - /> 217 - {error && ( 218 - <Text fz={'sm'} c={'red'}> 219 - {error} 220 - </Text> 221 - )} 222 - </Stack> 223 - </Stack> 224 - 225 - <Button type="submit" disabled={isLoading} loading={isLoading}> 226 - {isLoading ? 'Signing in...' : 'Sign in'} 227 - </Button> 228 - 229 - <Stack> 230 - <Button 231 - type="button" 232 - onClick={() => setUseAppPassword(false)} 233 - variant="transparent" 234 - color="blue" 235 - > 236 - Back to OAuth sign in 237 - </Button> 238 - </Stack> 239 - </Stack> 240 - </form> 241 - )} 242 - </Card> 49 + <Text fw={500} fz={'xl'}> 50 + Already logged in, redirecting you to library 51 + </Text> 52 + <Loader type="dots" /> 243 53 </Stack> 244 - </Center> 245 - ); 246 - } 54 + ); 55 + } 247 56 248 - export default function LoginPage() { 249 57 return ( 250 - <Suspense 251 - fallback={ 252 - <Center h={'100svh'}> 253 - <Stack align="center"> 254 - <Loader size="lg" /> 255 - </Stack> 256 - </Center> 257 - } 258 - > 259 - <LoginForm /> 58 + <Suspense> 59 + <Stack gap="xl" maw={300}> 60 + <Stack gap={'xs'}> 61 + <Image 62 + src={SembleLogo.src} 63 + alt="Semble logo" 64 + w={48} 65 + h={64.5} 66 + mx={'auto'} 67 + /> 68 + <Title order={1} ta="center"> 69 + Welcome back 70 + </Title> 71 + </Stack> 72 + <LoginForm /> 73 + <Stack align="center" gap={0}> 74 + <Text fw={500} c={'stone'}> 75 + {"Don't have an account?"} 76 + <Anchor href="/signup" fw={500}> 77 + Sign up 78 + </Anchor> 79 + </Text> 80 + <Popover withArrow shadow="md"> 81 + <PopoverTarget> 82 + <Button 83 + variant="white" 84 + size="md" 85 + fw={500} 86 + fs={'italic'} 87 + c={'stone'} 88 + rightSection={<IoMdHelpCircleOutline size={22} />} 89 + > 90 + How your Cosmik Network account works 91 + </Button> 92 + </PopoverTarget> 93 + <PopoverDropdown> 94 + <Text fw={500} ta="center" maw={380}> 95 + When you sign up today, you’ll create a Bluesky account. In near 96 + future, your account will be seamlessly migrated to our{' '} 97 + <Anchor 98 + href="https://cosmik.network" 99 + target="_blank" 100 + fw={500} 101 + c={'blue'} 102 + > 103 + Cosmik Network 104 + </Anchor> 105 + . 106 + </Text> 107 + </PopoverDropdown> 108 + </Popover> 109 + </Stack> 110 + </Stack> 260 111 </Suspense> 261 112 ); 262 113 }
+1
src/webapp/app/opengraph-image.alt
··· 1 + A social knowledge network for researchers
src/webapp/app/opengraph-image.jpg

This is a binary file and will not be displayed.

+217 -14
src/webapp/app/page.tsx
··· 1 - 'use client'; 2 - 3 - import { useAuth } from '@/hooks/useAuth'; 4 - import { useEffect } from 'react'; 5 - import { useRouter } from 'next/navigation'; 6 - import LandingPage from '@/components/LandingPage'; 1 + import { 2 + ActionIcon, 3 + SimpleGrid, 4 + Image, 5 + Text, 6 + BackgroundImage, 7 + Title, 8 + Stack, 9 + Button, 10 + Container, 11 + Box, 12 + Center, 13 + Group, 14 + Anchor, 15 + } from '@mantine/core'; 16 + import { FaBluesky, FaGithub, FaDiscord } from 'react-icons/fa6'; 17 + import { BiRightArrowAlt } from 'react-icons/bi'; 18 + import BG from '@/assets/semble-bg.webp'; 19 + import CosmikLogo from '@/assets/cosmik-logo-full.svg'; 20 + import CurateIcon from '@/assets/icons/curate-icon.svg'; 21 + import CommunityIcon from '@/assets/icons/community-icon.svg'; 22 + import DBIcon from '@/assets/icons/db-icon.svg'; 23 + import BigPictureIcon from '@/assets/icons/big-picture-icon.svg'; 24 + import SembleLogo from '@/assets/semble-logo.svg'; 7 25 8 26 export default function Home() { 9 - const { isAuthenticated, isLoading } = useAuth(); 10 - const router = useRouter(); 27 + return ( 28 + <BackgroundImage src={BG.src} h={'100svh'}> 29 + <Center h={'100svh'} py={{ base: '2rem', xs: '5rem' }}> 30 + <Container size={'xl'} p={'md'} my={'auto'}> 31 + <Stack align="center" gap={'5rem'}> 32 + <Stack gap={'xs'} align="center" maw={550} mx={'auto'}> 33 + <Image src={SembleLogo.src} alt="Semble logo" w={'auto'} h={60} /> 34 + <Title order={1} fw={600} fz={'3rem'} ta={'center'}> 35 + A social knowledge network for researchers 36 + </Title> 37 + <Title order={2} fw={600} fz={'xl'} c={'#1F6144'} ta={'center'}> 38 + Follow your peers’ research trails. Surface and discover new 39 + connections. Built on ATProto so you own your data. 40 + </Title> 41 + {process.env.VERCEL_ENV === 'production' ? ( 42 + <Button 43 + component="a" 44 + href="https://forms.cosmik.network/waitlist" 45 + target="_blank" 46 + size="lg" 47 + color="dark" 48 + mt={'lg'} 49 + > 50 + Join waitlist 51 + </Button> 52 + ) : ( 53 + <Group gap="md" mt={'lg'}> 54 + <Button component="a" href="/signup" size="lg"> 55 + Sign up 56 + </Button> 11 57 12 - useEffect(() => { 13 - if (isAuthenticated && !isLoading) { 14 - router.push('/library'); 15 - } 16 - }, [isAuthenticated, isLoading, router]); 58 + <Button 59 + component="a" 60 + href="/login" 61 + size="lg" 62 + color="dark" 63 + rightSection={<BiRightArrowAlt size={22} />} 64 + > 65 + Log in 66 + </Button> 67 + </Group> 68 + )} 69 + </Stack> 70 + 71 + <SimpleGrid 72 + cols={{ base: 1, xs: 2, sm: 2, md: 3, lg: 4 }} 73 + spacing={{ base: 'xl' }} 74 + mt={{ base: '1rem', xs: '5rem' }} 75 + > 76 + <Stack gap={'xs'}> 77 + <Image src={CurateIcon.src} alt="Curate icon" w={28} /> 78 + <Text> 79 + <Text fw={600} fz={'lg'} span> 80 + Curate your research trails. 81 + </Text>{' '} 82 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 83 + Collect interesting links, add notes, and organize them into 84 + shareable collections. Build trails others can explore and 85 + extend. 86 + </Text> 87 + </Text> 88 + </Stack> 89 + <Stack gap={'xs'}> 90 + <Image src={CommunityIcon.src} alt="Community icon" w={28} /> 91 + <Text> 92 + <Text fw={600} fz={'lg'} span> 93 + Connect with peers. 94 + </Text>{' '} 95 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 96 + See what your peers are sharing and find new collaborators 97 + with shared interests. Experience research rabbit holes, 98 + together. 99 + </Text> 100 + </Text> 101 + </Stack> 102 + <Stack gap={'xs'}> 103 + <Image src={DBIcon.src} alt="Database icon" w={28} /> 104 + <Text> 105 + <Text fw={600} fz={'lg'} span> 106 + Own your data. 107 + </Text>{' '} 108 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 109 + Built on ATProto, new apps will come to you. No more 110 + rebuilding your social graph and data when apps pivot and 111 + shut down. 112 + </Text> 113 + </Text> 114 + </Stack> 115 + <Stack gap={'xs'}> 116 + <Image src={BigPictureIcon.src} alt="Big picture icon" w={28} /> 117 + <Text> 118 + <Text fw={600} fz={'lg'} span> 119 + See the bigger picture. 120 + </Text>{' '} 121 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 122 + Find relevant research based on your network. Get the extra 123 + context that matters before you dive into a long read. 124 + </Text> 125 + </Text> 126 + </Stack> 127 + </SimpleGrid> 17 128 18 - return <LandingPage />; 129 + <Box 130 + component="footer" 131 + px={'md'} 132 + py={'xs'} 133 + mt={'xl'} 134 + pos={'relative'} 135 + > 136 + <Stack align="center" gap={'xs'}> 137 + <Text c="dark.1" fw={600} ta="center"> 138 + Made by &nbsp; 139 + <Anchor 140 + href="https://cosmik.network/" 141 + target="_blank" 142 + style={{ verticalAlign: 'middle' }} 143 + > 144 + <Box 145 + component="span" 146 + display="inline-flex" 147 + style={{ verticalAlign: 'middle' }} 148 + > 149 + <Image 150 + src={CosmikLogo.src} 151 + alt="Cosmik logo" 152 + w={92} 153 + h={28.4} 154 + /> 155 + </Box> 156 + </Anchor> 157 + &nbsp;&nbsp; 158 + <Text c="dark.1" fw={600} span> 159 + with support from&nbsp; 160 + <Anchor 161 + href="https://www.openphilanthropy.org/" 162 + target="_blank" 163 + c="dark.2" 164 + fw={600} 165 + > 166 + Open Philanthropy 167 + </Anchor>{' '} 168 + and{' '} 169 + <Anchor 170 + href="https://astera.org/" 171 + target="_blank" 172 + c="dark.2" 173 + fw={600} 174 + > 175 + Astera 176 + </Anchor> 177 + </Text> 178 + </Text> 179 + <Group gap="0"> 180 + <ActionIcon 181 + component="a" 182 + href="https://bsky.app/profile/cosmik.network" 183 + target="_blank" 184 + variant="subtle" 185 + color={'dark.2'} 186 + radius={'xl'} 187 + size={'xl'} 188 + m={0} 189 + > 190 + <FaBluesky size={22} /> 191 + </ActionIcon> 192 + <ActionIcon 193 + component="a" 194 + href="https://github.com/cosmik-network" 195 + target="_blank" 196 + variant="subtle" 197 + color={'dark.2'} 198 + radius={'xl'} 199 + size={'xl'} 200 + > 201 + <FaGithub size={22} /> 202 + </ActionIcon> 203 + <ActionIcon 204 + component="a" 205 + href="https://discord.gg/SHvvysb73e" 206 + target="_blank" 207 + variant="subtle" 208 + color={'dark.2'} 209 + radius={'xl'} 210 + size={'xl'} 211 + > 212 + <FaDiscord size={22} /> 213 + </ActionIcon> 214 + </Group> 215 + </Stack> 216 + </Box> 217 + </Stack> 218 + </Container> 219 + </Center> 220 + </BackgroundImage> 221 + ); 19 222 }
+6 -2
src/webapp/app/signup/layout.tsx
··· 1 - import { Container } from '@mantine/core'; 1 + import { Center, Container } from '@mantine/core'; 2 2 import { Metadata } from 'next'; 3 3 4 4 export const metadata: Metadata = { ··· 11 11 } 12 12 13 13 export default function Layout(props: Props) { 14 - return <Container p={'sm'}>{props.children}</Container>; 14 + return ( 15 + <Container p={'sm'}> 16 + <Center h={'100svh'}>{props.children}</Center> 17 + </Container> 18 + ); 15 19 }
+70 -41
src/webapp/app/signup/page.tsx
··· 1 - import { Title, Text, Stack, Button, Center, Anchor } from '@mantine/core'; 2 - import { FaBluesky } from 'react-icons/fa6'; 1 + 'use client'; 2 + 3 + import SignUpForm from '@/features/auth/components/signUpForm/SignUpForm'; 4 + import { Stack, Title, Text, Anchor, Image, Loader } from '@mantine/core'; 5 + import SembleLogo from '@/assets/semble-logo.svg'; 6 + import { useAuth } from '@/hooks/useAuth'; 7 + import { useEffect, useState } from 'react'; 8 + import { useRouter } from 'next/navigation'; 3 9 4 10 export default function Page() { 11 + const { isAuthenticated, isLoading } = useAuth(); 12 + const [isRedirecting, setIsRedirecting] = useState(false); 13 + const router = useRouter(); 14 + 15 + useEffect(() => { 16 + if (isAuthenticated) { 17 + setIsRedirecting(true); 18 + 19 + // redirect after 1 second 20 + const id = setTimeout(() => { 21 + router.push('/library'); 22 + }, 1000); 23 + } 24 + }, [isAuthenticated, router]); 25 + 26 + if (isLoading) { 27 + return ( 28 + <Stack align="center"> 29 + <Loader type="dots" /> 30 + </Stack> 31 + ); 32 + } 33 + 34 + if (isRedirecting) { 35 + return ( 36 + <Stack align="center"> 37 + <Text fw={500} fz={'xl'}> 38 + Already logged in, redirecting you to library 39 + </Text> 40 + <Loader type="dots" /> 41 + </Stack> 42 + ); 43 + } 44 + 5 45 return ( 6 - <Center h={'100svh'}> 7 - <Stack align="center" gap="xl" maw={450}> 8 - <Stack gap={0}> 46 + <Stack align="center" gap="xl" maw={450}> 47 + <Stack gap={0}> 48 + <Stack gap={'xs'}> 49 + <Image 50 + src={SembleLogo.src} 51 + alt="Semble logo" 52 + w={48} 53 + h={64.5} 54 + mx={'auto'} 55 + /> 9 56 <Title order={1} ta="center"> 10 57 Welcome 11 58 </Title> 12 - <Text fz={'h3'} fw={700} ta={'center'} c={'gray'}> 13 - Sign up to get started 14 - </Text> 15 59 </Stack> 16 - 17 - <Text fw={500} ta="center" maw={380}> 18 - When you sign up today, you’ll create a Bluesky account. In near 19 - future, your account will be seamlessly migrated to our{' '} 20 - <Anchor 21 - href="https://cosmik.network" 22 - target="_blank" 23 - fw={500} 24 - c={'blue'} 25 - > 26 - Cosmik Network 27 - </Anchor> 28 - . 60 + <Text fz={'h3'} fw={700} ta={'center'} c={'stone'}> 61 + Sign up to get started 29 62 </Text> 63 + </Stack> 30 64 31 - <Stack gap="xs"> 32 - <Button 33 - component="a" 34 - href="https://bsky.app/" 35 - target="_blank" 36 - size="lg" 37 - color="dark" 38 - leftSection={<FaBluesky size={22} />} 39 - > 40 - Sign up on Bluesky 41 - </Button> 42 - <Text fw={500} c={'gray'}> 43 - Already have an account?{' '} 44 - <Anchor href="/login" fw={500}> 45 - Log in 46 - </Anchor> 47 - </Text> 48 - </Stack> 49 - </Stack> 50 - </Center> 65 + <Text fw={500} ta="center" maw={380}> 66 + When you sign up today, you’ll create a Bluesky account. In near future, 67 + your account will be seamlessly migrated to our{' '} 68 + <Anchor 69 + href="https://cosmik.network" 70 + target="_blank" 71 + fw={500} 72 + c={'blue'} 73 + > 74 + Cosmik Network 75 + </Anchor> 76 + . 77 + </Text> 78 + <SignUpForm /> 79 + </Stack> 51 80 ); 52 81 }
+6
src/webapp/assets/semble-logo.svg
··· 1 + <svg width="32" height="43" viewBox="0 0 32 43" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M31.0164 33.1306C31.0164 38.581 25.7882 42.9994 15.8607 42.9994C5.93311 42.9994 0 37.5236 0 32.0732C0 26.6228 5.93311 23.2617 15.8607 23.2617C25.7882 23.2617 31.0164 27.6802 31.0164 33.1306Z" fill="#FF6400"/> 3 + <path d="M25.7295 19.3862C25.7295 22.5007 20.7964 22.2058 15.1558 22.2058C9.51511 22.2058 4.93445 22.1482 4.93445 19.0337C4.93445 15.9192 9.71537 12.6895 15.356 12.6895C20.9967 12.6895 25.7295 16.2717 25.7295 19.3862Z" fill="#FF6400"/> 4 + <path d="M25.0246 10.9256C25.0246 14.0401 20.7964 11.9829 15.1557 11.9829C9.51506 11.9829 6.34424 13.6876 6.34424 10.5731C6.34424 7.45857 9.51506 5.63867 15.1557 5.63867C20.7964 5.63867 25.0246 7.81103 25.0246 10.9256Z" fill="#FF6400"/> 5 + <path d="M20.4426 3.5755C20.4426 5.8323 18.2088 4.22951 15.2288 4.22951C12.2489 4.22951 10.5737 5.8323 10.5737 3.5755C10.5737 1.31871 12.2489 0 15.2288 0C18.2088 0 20.4426 1.31871 20.4426 3.5755Z" fill="#FF6400"/> 6 + </svg>
+174 -179
src/webapp/components/LandingPage.tsx
··· 22 22 import CommunityIcon from '@/assets/icons/community-icon.svg'; 23 23 import DBIcon from '@/assets/icons/db-icon.svg'; 24 24 import BigPictureIcon from '@/assets/icons/big-picture-icon.svg'; 25 + import SembleLogo from '@/assets/semble-logo.svg'; 25 26 26 27 export default function LandingPage() { 27 28 return ( 28 - <> 29 - <BackgroundImage src={BG.src} h={'100svh'}> 30 - <Center h={'100svh'} py={{ base: '2rem', xs: '5rem' }}> 31 - <Container size={'xl'} p={'md'} my={'auto'}> 32 - <Stack align="center" gap={'5rem'}> 33 - <Stack gap={'xs'} align="center" maw={550} mx={'auto'}> 34 - <Title order={1} fw={600} fz={'3rem'} ta={'center'}> 35 - A social knowledge network for researchers 36 - </Title> 37 - <Title order={2} fw={600} fz={'xl'} c={'#1F6144'} ta={'center'}> 38 - Follow your peers’ research trails. Surface and discover new 39 - connections. Built on ATProto so you own your data. 40 - </Title> 41 - {process.env.VERCEL_ENV === 'production' ? ( 42 - <Button 43 - component="a" 44 - href="https://forms.cosmik.network/waitlist" 45 - target="_blank" 46 - size="lg" 47 - color="dark" 48 - mt={'lg'} 49 - > 50 - Join waitlist 29 + <BackgroundImage src={BG.src} h={'100svh'}> 30 + <Center h={'100svh'} py={{ base: '2rem', xs: '5rem' }}> 31 + <Container size={'xl'} p={'md'} my={'auto'}> 32 + <Stack align="center" gap={'5rem'}> 33 + <Stack gap={'xs'} align="center" maw={550} mx={'auto'}> 34 + <Image src={SembleLogo.src} alt="Semble logo" w={'auto'} h={60} /> 35 + <Title order={1} fw={600} fz={'3rem'} ta={'center'}> 36 + A social knowledge network for researchers 37 + </Title> 38 + <Title order={2} fw={600} fz={'xl'} c={'#1F6144'} ta={'center'}> 39 + Follow your peers’ research trails. Surface and discover new 40 + connections. Built on ATProto so you own your data. 41 + </Title> 42 + {process.env.VERCEL_ENV === 'production' ? ( 43 + <Button 44 + component="a" 45 + href="https://forms.cosmik.network/waitlist" 46 + target="_blank" 47 + size="lg" 48 + color="dark" 49 + mt={'lg'} 50 + > 51 + Join waitlist 52 + </Button> 53 + ) : ( 54 + <Group gap="md" mt={'lg'}> 55 + <Button component="a" href="/signup" size="lg"> 56 + Sign up 57 + </Button> 58 + <Button component="a" href="/login" size="lg" color="dark"> 59 + Log in 51 60 </Button> 52 - ) : ( 53 - <Group gap="md" mt={'lg'}> 54 - <Button component="a" href="/signup" size="lg"> 55 - Sign up 56 - </Button> 57 - <Button component="a" href="/login" size="lg" color="dark"> 58 - Log in 59 - </Button> 60 - </Group> 61 - )} 62 - </Stack> 61 + </Group> 62 + )} 63 + </Stack> 63 64 64 - <SimpleGrid 65 - cols={{ base: 1, xs: 2, sm: 2, md: 3, lg: 4 }} 66 - spacing={{ base: 'xl' }} 67 - mt={{ base: '1rem', xs: '5rem' }} 68 - > 69 - <Stack gap={'xs'}> 70 - <Image src={CurateIcon.src} alt="Curate icon" w={28} /> 71 - <Text> 72 - <Text fw={600} fz={'lg'} span> 73 - Curate your research trails. 74 - </Text>{' '} 75 - <Text fw={500} fz={'lg'} c={'dark.2'} span> 76 - Collect interesting links, add notes, and organize them 77 - into shareable collections. Build trails others can 78 - explore and extend. 79 - </Text> 65 + <SimpleGrid 66 + cols={{ base: 1, xs: 2, sm: 2, md: 3, lg: 4 }} 67 + spacing={{ base: 'xl' }} 68 + mt={{ base: '1rem', xs: '5rem' }} 69 + > 70 + <Stack gap={'xs'}> 71 + <Image src={CurateIcon.src} alt="Curate icon" w={28} /> 72 + <Text> 73 + <Text fw={600} fz={'lg'} span> 74 + Curate your research trails. 75 + </Text>{' '} 76 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 77 + Collect interesting links, add notes, and organize them into 78 + shareable collections. Build trails others can explore and 79 + extend. 80 80 </Text> 81 - </Stack> 82 - <Stack gap={'xs'}> 83 - <Image src={CommunityIcon.src} alt="Community icon" w={28} /> 84 - <Text> 85 - <Text fw={600} fz={'lg'} span> 86 - Connect with peers. 87 - </Text>{' '} 88 - <Text fw={500} fz={'lg'} c={'dark.2'} span> 89 - See what your peers are sharing and find new collaborators 90 - with shared interests. Experience research rabbit holes, 91 - together. 92 - </Text> 81 + </Text> 82 + </Stack> 83 + <Stack gap={'xs'}> 84 + <Image src={CommunityIcon.src} alt="Community icon" w={28} /> 85 + <Text> 86 + <Text fw={600} fz={'lg'} span> 87 + Connect with peers. 88 + </Text>{' '} 89 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 90 + See what your peers are sharing and find new collaborators 91 + with shared interests. Experience research rabbit holes, 92 + together. 93 93 </Text> 94 - </Stack> 95 - <Stack gap={'xs'}> 96 - <Image src={DBIcon.src} alt="Database icon" w={28} /> 97 - <Text> 98 - <Text fw={600} fz={'lg'} span> 99 - Own your data. 100 - </Text>{' '} 101 - <Text fw={500} fz={'lg'} c={'dark.2'} span> 102 - Built on ATProto, new apps will come to you. No more 103 - rebuilding your social graph and data when apps pivot and 104 - shut down. 105 - </Text> 94 + </Text> 95 + </Stack> 96 + <Stack gap={'xs'}> 97 + <Image src={DBIcon.src} alt="Database icon" w={28} /> 98 + <Text> 99 + <Text fw={600} fz={'lg'} span> 100 + Own your data. 101 + </Text>{' '} 102 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 103 + Built on ATProto, new apps will come to you. No more 104 + rebuilding your social graph and data when apps pivot and 105 + shut down. 106 106 </Text> 107 - </Stack> 108 - <Stack gap={'xs'}> 109 - <Image 110 - src={BigPictureIcon.src} 111 - alt="Big picture icon" 112 - w={28} 113 - /> 114 - <Text> 115 - <Text fw={600} fz={'lg'} span> 116 - See the bigger picture. 117 - </Text>{' '} 118 - <Text fw={500} fz={'lg'} c={'dark.2'} span> 119 - Find relevant research based on your network. Get the 120 - extra context that matters before you dive into a long 121 - read. 122 - </Text> 107 + </Text> 108 + </Stack> 109 + <Stack gap={'xs'}> 110 + <Image src={BigPictureIcon.src} alt="Big picture icon" w={28} /> 111 + <Text> 112 + <Text fw={600} fz={'lg'} span> 113 + See the bigger picture. 114 + </Text>{' '} 115 + <Text fw={500} fz={'lg'} c={'dark.2'} span> 116 + Find relevant research based on your network. Get the extra 117 + context that matters before you dive into a long read. 123 118 </Text> 124 - </Stack> 125 - </SimpleGrid> 119 + </Text> 120 + </Stack> 121 + </SimpleGrid> 126 122 127 - <Box 128 - component="footer" 129 - px={'md'} 130 - py={'xs'} 131 - mt={'xl'} 132 - pos={'relative'} 133 - > 134 - <Stack align="center" gap={'xs'}> 135 - <Text c="dark.1" fw={600} ta="center"> 136 - Made by &nbsp; 137 - <Anchor 138 - href="https://cosmik.network/" 139 - target="_blank" 123 + <Box 124 + component="footer" 125 + px={'md'} 126 + py={'xs'} 127 + mt={'xl'} 128 + pos={'relative'} 129 + > 130 + <Stack align="center" gap={'xs'}> 131 + <Text c="dark.1" fw={600} ta="center"> 132 + Made by &nbsp; 133 + <Anchor 134 + href="https://cosmik.network/" 135 + target="_blank" 136 + style={{ verticalAlign: 'middle' }} 137 + > 138 + <Box 139 + component="span" 140 + display="inline-flex" 140 141 style={{ verticalAlign: 'middle' }} 141 142 > 142 - <Box 143 - component="span" 144 - display="inline-flex" 145 - style={{ verticalAlign: 'middle' }} 146 - > 147 - <Image 148 - src={CosmikLogo.src} 149 - alt="Cosmik logo" 150 - w={92} 151 - h={28.4} 152 - /> 153 - </Box> 154 - </Anchor> 155 - &nbsp;&nbsp; 156 - <Text c="dark.1" fw={600} span> 157 - with support from&nbsp; 158 - <Anchor 159 - href="https://www.openphilanthropy.org/" 160 - target="_blank" 161 - c="dark.2" 162 - fw={600} 163 - > 164 - Open Philanthropy 165 - </Anchor>{' '} 166 - and{' '} 167 - <Anchor 168 - href="https://astera.org/" 169 - target="_blank" 170 - c="dark.2" 171 - fw={600} 172 - > 173 - Astera 174 - </Anchor> 175 - </Text> 176 - </Text> 177 - <Group gap="0"> 178 - <ActionIcon 179 - component="a" 180 - href="https://bsky.app/profile/cosmik.network" 181 - target="_blank" 182 - variant="subtle" 183 - color={'dark.2'} 184 - radius={'xl'} 185 - size={'xl'} 186 - m={0} 187 - > 188 - <FaBluesky size={22} /> 189 - </ActionIcon> 190 - <ActionIcon 191 - component="a" 192 - href="https://github.com/cosmik-network" 143 + <Image 144 + src={CosmikLogo.src} 145 + alt="Cosmik logo" 146 + w={92} 147 + h={28.4} 148 + /> 149 + </Box> 150 + </Anchor> 151 + &nbsp;&nbsp; 152 + <Text c="dark.1" fw={600} span> 153 + with support from&nbsp; 154 + <Anchor 155 + href="https://www.openphilanthropy.org/" 193 156 target="_blank" 194 - variant="subtle" 195 - color={'dark.2'} 196 - radius={'xl'} 197 - size={'xl'} 157 + c="dark.2" 158 + fw={600} 198 159 > 199 - <FaGithub size={22} /> 200 - </ActionIcon> 201 - <ActionIcon 202 - component="a" 203 - href="https://discord.gg/SHvvysb73e" 160 + Open Philanthropy 161 + </Anchor>{' '} 162 + and{' '} 163 + <Anchor 164 + href="https://astera.org/" 204 165 target="_blank" 205 - variant="subtle" 206 - color={'dark.2'} 207 - radius={'xl'} 208 - size={'xl'} 166 + c="dark.2" 167 + fw={600} 209 168 > 210 - <FaDiscord size={22} /> 211 - </ActionIcon> 212 - </Group> 213 - </Stack> 214 - </Box> 215 - </Stack> 216 - </Container> 217 - </Center> 218 - </BackgroundImage> 219 - </> 169 + Astera 170 + </Anchor> 171 + </Text> 172 + </Text> 173 + <Group gap="0"> 174 + <ActionIcon 175 + component="a" 176 + href="https://bsky.app/profile/cosmik.network" 177 + target="_blank" 178 + variant="subtle" 179 + color={'dark.2'} 180 + radius={'xl'} 181 + size={'xl'} 182 + m={0} 183 + > 184 + <FaBluesky size={22} /> 185 + </ActionIcon> 186 + <ActionIcon 187 + component="a" 188 + href="https://github.com/cosmik-network" 189 + target="_blank" 190 + variant="subtle" 191 + color={'dark.2'} 192 + radius={'xl'} 193 + size={'xl'} 194 + > 195 + <FaGithub size={22} /> 196 + </ActionIcon> 197 + <ActionIcon 198 + component="a" 199 + href="https://discord.gg/SHvvysb73e" 200 + target="_blank" 201 + variant="subtle" 202 + color={'dark.2'} 203 + radius={'xl'} 204 + size={'xl'} 205 + > 206 + <FaDiscord size={22} /> 207 + </ActionIcon> 208 + </Group> 209 + </Stack> 210 + </Box> 211 + </Stack> 212 + </Container> 213 + </Center> 214 + </BackgroundImage> 220 215 ); 221 216 }
+254
src/webapp/features/auth/components/loginForm/LoginForm.tsx
··· 1 + 'use client'; 2 + 3 + import { ExtensionService } from '@/services/extensionService'; 4 + import { ApiClient } from '@/api-client/ApiClient'; 5 + import { 6 + Stack, 7 + Text, 8 + Button, 9 + UnstyledButton, 10 + Anchor, 11 + Popover, 12 + TextInput, 13 + PasswordInput, 14 + Alert, 15 + } from '@mantine/core'; 16 + import { BiRightArrowAlt } from 'react-icons/bi'; 17 + import { IoMdHelpCircleOutline } from 'react-icons/io'; 18 + import { MdOutlineAlternateEmail, MdLock } from 'react-icons/md'; 19 + import { useAuth } from '@/hooks/useAuth'; 20 + import { getAccessToken } from '@/services/auth'; 21 + import { useForm } from '@mantine/form'; 22 + import { useRouter, useSearchParams } from 'next/navigation'; 23 + import { useEffect, useState } from 'react'; 24 + 25 + export default function LoginForm() { 26 + const router = useRouter(); 27 + const searchParams = useSearchParams(); 28 + const { setTokens, isAuthenticated } = useAuth(); 29 + 30 + const [isCheckingAuth, setIsCheckingAuth] = useState(true); 31 + const [isLoading, setIsLoading] = useState(false); 32 + const [error, setError] = useState(''); 33 + 34 + const isExtensionLogin = searchParams.get('extension-login') === 'true'; 35 + const apiClient = new ApiClient( 36 + process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000', 37 + () => getAccessToken(), 38 + ); 39 + 40 + const handleExtensionTokenGeneration = async () => { 41 + try { 42 + setIsLoading(true); 43 + const tokens = await apiClient.generateExtensionTokens(); 44 + 45 + await ExtensionService.sendTokensToExtension(tokens); 46 + 47 + setError(''); 48 + 49 + // Clear the extension tokens requested flag 50 + ExtensionService.clearExtensionTokensRequested(); 51 + 52 + // Redirect to extension success page after successful extension token generation 53 + router.push('/extension/auth/complete'); 54 + } catch (err: any) { 55 + // Clear the flag even on failure 56 + ExtensionService.clearExtensionTokensRequested(); 57 + 58 + // Redirect to extension error page 59 + router.push('/extension/auth/error'); 60 + } finally { 61 + setIsLoading(false); 62 + } 63 + }; 64 + 65 + useEffect(() => { 66 + if (isAuthenticated) { 67 + isExtensionLogin 68 + ? handleExtensionTokenGeneration() 69 + : router.push('/library'); 70 + } else { 71 + setIsCheckingAuth(false); 72 + } 73 + }, [isAuthenticated, isExtensionLogin]); 74 + 75 + const handleOAuthSubmit = async (e: React.FormEvent) => { 76 + e.preventDefault(); 77 + 78 + // validate form 79 + const isValid = form.validateField('handle'); 80 + if (!isValid) return; 81 + 82 + try { 83 + setIsLoading(true); 84 + setError(''); 85 + 86 + if (isExtensionLogin) { 87 + ExtensionService.setExtensionTokensRequested(); 88 + } 89 + 90 + const { authUrl } = await apiClient.initiateOAuthSignIn({ 91 + handle: form.values.handle, 92 + }); 93 + 94 + window.location.href = authUrl; 95 + } catch (err: any) { 96 + setError(err.message || 'An error occurred during login'); 97 + } finally { 98 + setIsLoading(false); 99 + } 100 + }; 101 + 102 + const handleAppPasswordSubmit = async (e: React.FormEvent) => { 103 + e.preventDefault(); 104 + 105 + // validate 106 + const validation = form.validate(); 107 + if (validation.hasErrors) return; 108 + 109 + try { 110 + setIsLoading(true); 111 + setError(''); 112 + 113 + const { accessToken, refreshToken } = 114 + await apiClient.loginWithAppPassword({ 115 + identifier: form.values.handle, 116 + appPassword: form.values.appPassword, 117 + }); 118 + 119 + setTokens(accessToken, refreshToken); 120 + 121 + if (isExtensionLogin) { 122 + await handleExtensionTokenGeneration(); 123 + } else { 124 + router.push('/library'); 125 + } 126 + } catch (err: any) { 127 + setError(err.message || 'Invalid credentials'); 128 + } finally { 129 + setIsLoading(false); 130 + } 131 + }; 132 + 133 + const form = useForm({ 134 + initialValues: { 135 + handle: '', 136 + appPassword: '', 137 + useAppPassword: false, 138 + }, 139 + 140 + validate: { 141 + handle: (value) => (value.trim() ? null : 'Handle is required'), 142 + appPassword: (value, values) => 143 + values.useAppPassword && value.trim() === '' 144 + ? 'App password is required' 145 + : null, 146 + }, 147 + }); 148 + 149 + if (form.values.useAppPassword) { 150 + return ( 151 + <Stack gap={'lg'}> 152 + <form onSubmit={handleAppPasswordSubmit}> 153 + <Stack align="center"> 154 + <TextInput 155 + label="Handle" 156 + placeholder="you.bsky.social" 157 + key={form.key('handle')} 158 + value={form.values.handle} 159 + onChange={(event) => { 160 + form.setFieldValue('handle', event.currentTarget.value); 161 + if (error) setError(''); 162 + }} 163 + leftSection={<MdOutlineAlternateEmail size={22} />} 164 + variant="filled" 165 + size="lg" 166 + w={'100%'} 167 + required 168 + /> 169 + <PasswordInput 170 + label="App password" 171 + placeholder="Your assword" 172 + key={form.key('appPassword')} 173 + {...form.getInputProps('appPassword')} 174 + leftSection={<MdLock size={22} />} 175 + variant="filled" 176 + size="lg" 177 + w={'100%'} 178 + required 179 + /> 180 + <Button 181 + type="submit" 182 + size="lg" 183 + color="dark" 184 + fullWidth 185 + rightSection={<BiRightArrowAlt size={22} />} 186 + loading={isLoading} 187 + > 188 + Log in 189 + </Button> 190 + {error && <Alert variant="white" title={error} color="red" />} 191 + </Stack> 192 + </form> 193 + <Stack align="center"> 194 + <UnstyledButton 195 + fw={500} 196 + onClick={() => { 197 + form.setFieldValue('useAppPassword', false); 198 + setError(''); 199 + }} 200 + > 201 + Use OAuth login 202 + </UnstyledButton> 203 + </Stack> 204 + </Stack> 205 + ); 206 + } 207 + 208 + return ( 209 + <Stack gap={'xl'}> 210 + <form onSubmit={handleOAuthSubmit}> 211 + <Stack align="center"> 212 + <TextInput 213 + label="Handle" 214 + placeholder="you.bsky.social" 215 + key={form.key('handle')} 216 + value={form.values.handle} 217 + onChange={(event) => { 218 + form.setFieldValue('handle', event.currentTarget.value); 219 + if (error) setError(''); 220 + }} 221 + leftSection={<MdOutlineAlternateEmail size={22} />} 222 + variant="filled" 223 + size="lg" 224 + w={'100%'} 225 + required 226 + /> 227 + <Button 228 + type="submit" 229 + size="lg" 230 + color="dark" 231 + fullWidth 232 + rightSection={<BiRightArrowAlt size={22} />} 233 + loading={isLoading} 234 + > 235 + Log in 236 + </Button> 237 + {error && <Alert variant="white" title={error} color="red" />} 238 + <Text fw={500} c={'stone'}> 239 + Or 240 + </Text> 241 + <UnstyledButton 242 + fw={500} 243 + onClick={() => { 244 + form.setFieldValue('useAppPassword', true); 245 + setError(''); 246 + }} 247 + > 248 + Use your app password 249 + </UnstyledButton> 250 + </Stack> 251 + </form> 252 + </Stack> 253 + ); 254 + }
+25
src/webapp/features/auth/components/signUpForm/SignUpForm.tsx
··· 1 + import { Text, Stack, Button, Anchor } from '@mantine/core'; 2 + import { FaBluesky } from 'react-icons/fa6'; 3 + 4 + export default function SignUpForm() { 5 + return ( 6 + <Stack gap="xs"> 7 + <Button 8 + component="a" 9 + href="https://bsky.app/" 10 + target="_blank" 11 + size="lg" 12 + color="dark" 13 + leftSection={<FaBluesky size={22} />} 14 + > 15 + Sign up on Bluesky 16 + </Button> 17 + <Text fw={500} c={'stone'}> 18 + Already have an account?{' '} 19 + <Anchor href="/login" fw={500}> 20 + Log in 21 + </Anchor> 22 + </Text> 23 + </Stack> 24 + ); 25 + }
+2 -2
src/webapp/styles/theme.tsx
··· 1 1 'use client'; 2 2 3 - import { Button, createTheme } from '@mantine/core'; 3 + import { Button, createTheme, TextInput } from '@mantine/core'; 4 4 5 5 export const theme = createTheme({ 6 6 primaryColor: 'tangerine', ··· 17 17 '#cb4d00', 18 18 '#b14000', 19 19 ], 20 - gray: [ 20 + stone: [ 21 21 '#fff1f5', 22 22 '#ede6e7', 23 23 '#d0cccd',