-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
32 changed files
with
715 additions
and
270 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Makoto/v3 | ||
|
||
## Frontend | ||
### Main page | ||
- /: Welcome page | ||
- /me: Current user page | ||
- /me/edit: Current user edit page | ||
- /me/friends: User's friends | ||
______ | ||
- /: logo + text `Makoto` | ||
- /me: user's picture, username, join date, intergration (providers like Discord, Spotify) | edit button | microservice stat | ||
- /me/edit: | ||
1. update username, description, picture, preferred color, languages | ||
2. "who can see my user page?", "who can send me friend request?", "who can see my micro stats?", "who can message me?", "who can see my user picture?" | ||
3. integrations' settings, email settings (redirect) | ||
|
||
|
||
### Authentication | ||
- /: login | ||
- /signup | ||
- /confirm-email | ||
- /password/change | ||
- /password/recover | ||
______ | ||
- SignIn via OAuth2/Credentials | ||
- SignUp via credentials | ||
- Confirm email when via credentials | ||
- Change/Recover password for credentials | ||
- Max SignIn tries ~ 5 -> timeout | ||
- Error messages | ||
- Clarify whether username/email is reserved | ||
- Password strongness validation | ||
|
||
## Backend | ||
### Authentication | ||
- SignIn | ||
- SignUp | ||
- SignInOAuth | ||
- SendConfirmationMail (no) | ||
- ConfirmMailByCode (tends to be hashed username -> compare hash maybe?) | ||
- ProceedToUpdatePassword ( using old password ) | ||
- UpdatePassword ( via some sort of generated and stored in db (or redis) token ) | ||
- SendRecoverPasswordMail (no) | ||
- SubmitNewPasswordByRecoverdCode ( same sort token as for UpdatePassword) | ||
- IsEmailAvailable | ||
- IsUsernameAvailable | ||
_______ | ||
|
||
### Mail Service | ||
- SendEmail | ||
|
||
### CDN | ||
|
||
### Server Registry && Discovery | ||
- RegisterService | ||
- UnregisterService | ||
_____ | ||
- Redis ( as key-value pairs ) | ||
|
||
### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,21 @@ | ||
'use client' | ||
import { Dialog, DialogTrigger, DialogContent } from '$/components/ui/dialog' | ||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$/components/ui/tabs' | ||
import { Input } from '$/components/ui/input' | ||
import { Label } from '$/components/ui/label' | ||
import { useEffect, useState } from 'react' | ||
import { Separator } from '$/components/ui/separator' | ||
import { Textarea } from '$/components/ui/textarea' | ||
import dynamic from 'next/dynamic' | ||
import { Button } from '$/components/ui/button' | ||
import Image from 'next/image' | ||
import { Switch } from '$/components/ui/switch' | ||
import { HexColorPicker } from 'react-colorful' | ||
import { Select, SelectItem, SelectContent, SelectGroup, SelectTrigger } from '$/components/ui/select' | ||
import { useTheme } from 'next-themes' | ||
|
||
const PictureEditor = dynamic(() => import('$/components/picture-editor'), { ssr: false }) | ||
import Privacy from './privacy' | ||
import Profile from './profile' | ||
|
||
const user = { | ||
name: 'dehwyy', | ||
email: '[email protected]', | ||
customId: '', | ||
description: "Hello, I'm dehwyy and using Makoto but way longer sentence to test if it works.", | ||
dark_background: '#171717', | ||
light_background: '#2ccce7', | ||
image: '', | ||
|
||
isVerifiedEmail: false, | ||
} | ||
|
||
const Page = () => { | ||
const initialName = 'dehwyy!' | ||
const initialEmail = '[email protected]' | ||
const customId = 'a0131d5d-6b2b-4b2a-8b5b-4b2a8b5b4b2a' | ||
const initialDescription = "Hello, I'm dehwyy and using Makoto but way longer sentence to test if it works." | ||
const dark_background = '#171717' | ||
const background = '#2ccce7' | ||
|
||
const { theme: currentTheme, setTheme } = useTheme() | ||
|
||
const [name, setName] = useState(initialName) | ||
const [email, setEmail] = useState(initialEmail) | ||
const [id, setId] = useState(customId) | ||
const [description, setDescription] = useState(initialDescription) | ||
|
||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false) | ||
const [bgTheme, setBgTheme] = useState<'light' | 'dark'>('dark') | ||
const [initialDarkBg, setInitialDarkBg] = useState(dark_background) | ||
const [darkBg, setDarkBg] = useState(dark_background) | ||
const [initialLightBg, setInitialLightBg] = useState(background) | ||
const [lightBg, setLightBg] = useState(background) | ||
|
||
useEffect(() => { | ||
isColorPickerOpen ? setTheme(bgTheme) : setBgTheme(currentTheme as 'light' | 'dark') | ||
}, [bgTheme]) | ||
|
||
const [image, setImage] = useState('') | ||
|
||
return ( | ||
<div className="min-h-screen mx-auto pt-28 w-[90%] md:w-2/3"> | ||
<Tabs defaultValue="profile" className="w-full"> | ||
|
@@ -54,236 +26,25 @@ const Page = () => { | |
|
||
{/* User profile */} | ||
<TabsContent value="profile"> | ||
<div className="p-5 dark:bg-black rounded-xl overflow-hidden border-2 border-secondary grid lg:grid-cols-2 gap-x-8"> | ||
<section className="font-Content px-3 flex flex-col gap-y-7"> | ||
<WithLabel id="display_name" text="display name"> | ||
<Input | ||
className="w-full mt-2" | ||
type="text" | ||
id="display_name" | ||
maxLength={20} | ||
placeholder={initialName} | ||
spellCheck={false} | ||
value={name} | ||
onChange={e => setName(e.target.value)} | ||
/> | ||
</WithLabel> | ||
<WithLabel id="email" text="email"> | ||
<Input | ||
className="w-full mt-2" | ||
type="email" | ||
id="email" | ||
autoComplete="email" | ||
placeholder={initialEmail} | ||
spellCheck={false} | ||
value={email} | ||
onChange={e => setEmail(e.target.value)} | ||
/> | ||
</WithLabel> | ||
<Separator /> | ||
<div className="grid grid-cols-2 gap-5"> | ||
<p className="font-sans text-sm font-[600] mb-2 col-span-2">BACKGROUND COLOR</p> | ||
<div className="w-full"> | ||
<div className="flex flex-col"> | ||
<Button | ||
onClick={() => { | ||
setIsColorPickerOpen(p => !p) | ||
setInitialDarkBg(darkBg) | ||
setInitialLightBg(lightBg) | ||
}} | ||
variant={isColorPickerOpen ? 'default' : 'outline'} | ||
className="w-full"> | ||
{isColorPickerOpen ? 'Save' : 'Edit'} | ||
</Button> | ||
{isColorPickerOpen && ( | ||
<Button | ||
onClick={() => { | ||
setIsColorPickerOpen(false) | ||
setLightBg(initialLightBg) | ||
setDarkBg(initialDarkBg) | ||
}} | ||
variant="destructive" | ||
className={`${ | ||
(bgTheme === 'light' && initialLightBg != lightBg) || (bgTheme === 'dark' && initialDarkBg != darkBg) | ||
? 'visible opacity-100 max-h-[41px] mt-2' | ||
: 'invisible opacity-0 max-h-[0px] py-0' | ||
} w-full transition-all`}> | ||
Discard | ||
</Button> | ||
)} | ||
</div> | ||
{isColorPickerOpen && ( | ||
<div className="pt-5 flex flex-col gap-y-3 select-none"> | ||
<p className="text-sm font-Content dark:text-gray-300 underline">Background depenends on theme!</p> | ||
<p className="text-sm dark:text-gray-300 -mb-2 text-center">Select for</p> | ||
<Select onValueChange={v => setBgTheme(v as 'light' | 'dark')}> | ||
<SelectTrigger>{bgTheme[0].toUpperCase() + bgTheme.substring(1)}</SelectTrigger> | ||
<SelectContent> | ||
<SelectGroup className="font-Content"> | ||
<SelectItem value="light">Light</SelectItem> | ||
<SelectItem value="dark">Dark</SelectItem> | ||
</SelectGroup> | ||
</SelectContent> | ||
</Select> | ||
<p className="text-sm dark:text-gray-300 text-center">Theme</p> | ||
</div> | ||
)} | ||
</div> | ||
<div | ||
className={ | ||
(isColorPickerOpen ? 'visible opacity-100 max-h-[300px]' : 'invisible opacity-0 max-h-[0px]') + | ||
' transition-all duraiton-300 ease-in-out w-[200px]' | ||
}> | ||
<p className="text-center text-lg underline">Pick a color</p> | ||
{bgTheme === 'light' ? ( | ||
<HexColorPicker color={lightBg} onChange={setLightBg} /> | ||
) : bgTheme === 'dark' ? ( | ||
<HexColorPicker color={darkBg} onChange={setDarkBg} /> | ||
) : ( | ||
<></> | ||
)} | ||
<Input | ||
className="mt-5" | ||
autoComplete="disabled" | ||
placeholder={bgTheme === 'light' ? lightBg : darkBg} | ||
spellCheck={false} | ||
value={bgTheme === 'light' ? lightBg : darkBg} | ||
onChange={e => { | ||
if (!e.target.value.startsWith('#')) return | ||
|
||
if (bgTheme === 'light') setLightBg(e.target.value) | ||
if (bgTheme === 'dark') setDarkBg(e.target.value) | ||
}} | ||
/> | ||
</div> | ||
</div> | ||
<Separator /> | ||
<WithLabel id="custom_id" text="custom id"> | ||
<Input | ||
className="w-full mt-2" | ||
type="text" | ||
id="custom_id" | ||
placeholder={id} | ||
autoComplete="disabled" | ||
spellCheck={false} | ||
value={id} | ||
onChange={e => setId(e.target.value)} | ||
/> | ||
</WithLabel> | ||
<Separator /> | ||
<WithLabel id="description" text="description"> | ||
<Textarea | ||
className="w-full mt-2" | ||
id="description" | ||
placeholder={initialDescription} | ||
spellCheck={false} | ||
value={description} | ||
onChange={e => setDescription(e.target.value)} | ||
/> | ||
</WithLabel> | ||
<Separator /> | ||
|
||
<div> | ||
<p className="font-sans text-sm font-[600] mb-2">AVATAR</p> | ||
<Dialog> | ||
<DialogTrigger className="block" asChild> | ||
<Button className="min-w-[50%]">Edit</Button> | ||
</DialogTrigger> | ||
<DialogContent> | ||
<PictureEditor onSave={image => setImage(image)} /> | ||
</DialogContent> | ||
</Dialog> | ||
</div> | ||
</section> | ||
<section> | ||
<p className="font-sans text-sm font-[600] mb-2">PREVIEW</p> | ||
<div className="dark:bg-black rounded-xl overflow-hidden border-2 border-secondary"> | ||
<div className="h-[20px] dark:bg-black flex items-center my-0.5"> | ||
<div className="bg-secondary rounded-md h-[13px] w-[93%] mx-auto px-2"> | ||
<span className="font-Content text-[10px] relative bottom-[9px] underline select-none">https://makoto/me/{id}</span> | ||
</div> | ||
</div> | ||
<div | ||
style={{ | ||
backgroundColor: currentTheme === 'light' ? lightBg : darkBg, | ||
}} | ||
className="h-[60px]" | ||
/> | ||
<div className="px-7 flex justify-between gap-x-7 pt-1"> | ||
<div className="dark:border-transparent border-secondary border-2 max-h-[110px] max-w-[110px] min-h-[110px] min-w-[110px] overflow-hidden rounded-full object-cover select-none relative bottom-4"> | ||
<Image priority={true} src={image || '/images/kawaii.png'} width={350} height={350} alt="avatar" /> | ||
</div> | ||
<div className="flex-auto self-center"> | ||
<h3 className="font-Jua text-2xl dark:font-[800] font-[500] tracking-wider break-all">{name || initialName}</h3> | ||
</div> | ||
</div> | ||
<div className="px-7 pb-5"> | ||
{/* integrations */} | ||
<div className="flex justify-between w-[110px] px-5"></div> | ||
|
||
{/* information */} | ||
<div className="pt-5 pl-3 flex flex-col gap-y-3"> | ||
<p className="font-ContentT font-[500]">{description}</p> | ||
</div> | ||
</div> | ||
</div> | ||
</section> | ||
</div> | ||
<Profile | ||
name={user.name} | ||
email={user.email} | ||
customId={user.customId} | ||
description={user.description} | ||
dark_background={user.dark_background} | ||
light_background={user.light_background} | ||
image={user.image} | ||
isVerifiedEmail={user.isVerifiedEmail} | ||
/> | ||
</TabsContent> | ||
|
||
{/* User privacy */} | ||
<TabsContent value="privacy"> | ||
<div className="p-5 dark:bg-black rounded-xl overflow-hidden border-2 border-secondary grid justify-items-center lg:grid-cols-2 gap-x-8"> | ||
<section> | ||
<h2 className="font-ContentT text-lg p-3 font-[600]">Profile privacy:</h2> | ||
<div className="flex flex-col gap-y-6 mt-7 mb-3 font-ContentT"> | ||
<Privacy text="Who can see my profile?"> | ||
<p>Nobody</p> | ||
<Switch /> | ||
<p>Everyone</p> | ||
</Privacy> | ||
<Privacy text="Who can follow me?"> | ||
<p>Nobody</p> | ||
<Switch /> | ||
<p>Everyone</p> | ||
</Privacy> | ||
</div> | ||
</section> | ||
<section> | ||
<h2 className="font-ContentT text-lg p-3 font-[600]">Services privacy:</h2> | ||
<div className="flex flex-col gap-y-6 mt-7 mb-3 font-ContentT"> | ||
<Privacy text="Who can see my services?"> | ||
<p>Nobody</p> | ||
<Switch /> | ||
<p>Everyone</p> | ||
</Privacy> | ||
</div> | ||
</section> | ||
</div> | ||
<Privacy /> | ||
</TabsContent> | ||
</Tabs> | ||
</div> | ||
) | ||
} | ||
|
||
const Privacy = (props: { children: React.ReactNode; text: string }) => { | ||
return ( | ||
<div className="flex flex-col gap-y-3"> | ||
<p className="underline">{props.text}</p> | ||
<div className="flex gap-x-3">{props.children}</div> | ||
</div> | ||
) | ||
} | ||
|
||
const WithLabel = (props: { children: React.ReactNode; text: string; id: string }) => { | ||
return ( | ||
<div> | ||
<Label htmlFor={props.id} className="font-sans text-sm font-[600]"> | ||
{props.text.toUpperCase()} | ||
</Label> | ||
{props.children} | ||
</div> | ||
) | ||
} | ||
|
||
export default Page |
Oops, something went wrong.