-
-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: allow user to change his password/email/name #20
Changes from all commits
cf4f7f5
845c576
9244bbf
af87779
020018b
f8e9889
44e6c5a
a85ae88
353a217
7ff47da
31bc8b5
7688adb
4bd7a69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { twMerge } from "tailwind-merge"; | ||
|
||
import type { HTMLAttributes } from "react"; | ||
|
||
const variants = { | ||
1: "text-2xl", | ||
2: "text-xl", | ||
3: "text-lg", | ||
4: "text-base", | ||
5: "text-sm", | ||
6: "text-xs", | ||
} as const; | ||
|
||
type HeadingProps = Readonly<{ | ||
level: keyof typeof variants; | ||
}> & | ||
HTMLAttributes<HTMLHeadingElement>; | ||
|
||
export const Heading = ({ level, className, ...props }: HeadingProps) => { | ||
const Tag = `h${level}` as const; | ||
|
||
return ( | ||
<Tag | ||
className={twMerge( | ||
"font-medium leading-6 text-gray-900", | ||
variants[level], | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Heading } from "../heading/Heading"; | ||
import { Tabs } from "../tabs/Tabs"; | ||
|
||
import type { ReactNode } from "react"; | ||
|
||
const tabs = [ | ||
{ | ||
label: "General", | ||
pathname: "/dashboard/settings", | ||
}, | ||
]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will make |
||
|
||
interface SettingsLayoutProps { | ||
readonly title: string; | ||
readonly children: ReactNode; | ||
} | ||
|
||
export const SettingsLayout = ({ title, children }: SettingsLayoutProps) => ( | ||
<section className="mx-auto mt-8 max-w-6xl px-4 sm:px-6 lg:mt-12 lg:px-8"> | ||
<Heading level={2} className="mb-8"> | ||
{title} | ||
</Heading> | ||
<Tabs tabs={tabs} /> | ||
{children} | ||
</section> | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { twMerge } from "tailwind-merge"; | ||
|
||
import { useZodForm } from "../../../hooks/useZodForm"; | ||
import { onPromise } from "../../../utils/functions"; | ||
import { Button } from "../../common/Button"; | ||
import { Input } from "../../common/Input"; | ||
|
||
import { | ||
LEFT_SECTION_STYLES, | ||
RIGHT_SECTION_STYLES, | ||
SECTIONS_WRAPPER_STYLES, | ||
} from "./SettingsRow"; | ||
|
||
import type { HTMLInputTypeAttribute } from "react"; | ||
import type { ZodObject, ZodString } from "zod"; | ||
|
||
interface RowFormProps { | ||
readonly schema: ZodObject<{ content: ZodString }>; | ||
readonly content: string; | ||
readonly contentType: HTMLInputTypeAttribute; | ||
readonly onSubmit: (content: string) => void; | ||
readonly onCancel?: () => void; | ||
} | ||
|
||
export const RowForm = ({ | ||
schema, | ||
content, | ||
contentType, | ||
onSubmit, | ||
onCancel, | ||
}: RowFormProps) => { | ||
const { | ||
handleFormSubmit, | ||
register, | ||
formState: { errors }, | ||
} = useZodForm( | ||
schema, | ||
{ | ||
onSubmit: ({ content }) => { | ||
onSubmit(content); | ||
}, | ||
}, | ||
{ defaultValues: { content } }, | ||
); | ||
|
||
return ( | ||
<form | ||
className={SECTIONS_WRAPPER_STYLES} | ||
onSubmit={onPromise(handleFormSubmit)} | ||
> | ||
<div className={LEFT_SECTION_STYLES}> | ||
<Input type={contentType} {...register("content")} /> | ||
{errors.content && ( | ||
<p className="mt-1 text-xs text-red-500">{errors.content.message}</p> | ||
)} | ||
</div> | ||
<div | ||
className={twMerge( | ||
RIGHT_SECTION_STYLES, | ||
errors.content && "mt-1 sm:items-start", | ||
)} | ||
> | ||
<Button type="submit">Save</Button> | ||
<Button type="button" onClick={onCancel}> | ||
Cancel | ||
</Button> | ||
</div> | ||
</form> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { useState } from "react"; | ||
|
||
import { Button, BUTTON_VARIANT } from "../../common/Button"; | ||
|
||
import { RowForm } from "./RowForm"; | ||
|
||
import type { HTMLInputTypeAttribute } from "react"; | ||
import type { ZodObject, ZodString } from "zod"; | ||
|
||
export const SECTIONS_WRAPPER_STYLES = | ||
"mt-1 flex items-start sm:col-span-2 sm:mt-0 sm:items-center"; | ||
export const LEFT_SECTION_STYLES = "grow text-gray-800"; | ||
export const RIGHT_SECTION_STYLES = | ||
"ml-2.5 flex h-full flex-shrink-0 flex-col items-stretch gap-1 font-medium sm:flex-row sm:items-center"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why consts? Just put it into element directly |
||
|
||
interface SettingsRowProps { | ||
readonly label: string; | ||
readonly schema: ZodObject<{ content: ZodString }>; | ||
readonly contentType: HTMLInputTypeAttribute; | ||
readonly content?: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
readonly onChange: (content: string) => void; | ||
} | ||
|
||
export const SettingsRow = ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. put this component in |
||
label, | ||
schema, | ||
contentType, | ||
content, | ||
onChange, | ||
}: SettingsRowProps) => { | ||
const [isEditMode, setIsEditMode] = useState(false); | ||
|
||
const handleFormSubmit = (content: string) => { | ||
onChange(content); | ||
setIsEditMode(false); | ||
}; | ||
|
||
return ( | ||
<li className="py-4 text-sm sm:grid sm:min-h-[4rem] sm:grid-cols-3 sm:gap-4 sm:py-2"> | ||
<div className="flex items-center font-medium text-gray-500">{label}</div> | ||
{isEditMode ? ( | ||
<RowForm | ||
schema={schema} | ||
content={content ?? ""} | ||
contentType={contentType} | ||
onSubmit={handleFormSubmit} | ||
onCancel={() => setIsEditMode(false)} | ||
/> | ||
) : ( | ||
<div className={SECTIONS_WRAPPER_STYLES}> | ||
<div className={LEFT_SECTION_STYLES}>{content}</div> | ||
<div className={RIGHT_SECTION_STYLES}> | ||
<Button | ||
type="button" | ||
variant={BUTTON_VARIANT.TEXT} | ||
onClick={() => setIsEditMode(true)} | ||
> | ||
Edit | ||
</Button> | ||
</div> | ||
</div> | ||
)} | ||
</li> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import Link from "next/link"; | ||
import { usePathname, useRouter } from "next/navigation"; | ||
import { twMerge } from "tailwind-merge"; | ||
|
||
import type { ChangeEvent } from "react"; | ||
|
||
interface Tab { | ||
readonly label: string; | ||
readonly pathname: string; | ||
} | ||
|
||
interface TabsProps { | ||
readonly tabs: Tab[]; | ||
} | ||
|
||
export const Tabs = ({ tabs }: TabsProps) => { | ||
const router = useRouter(); | ||
const pathname = usePathname(); | ||
|
||
const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => { | ||
const tab = tabs.find(({ pathname }) => pathname === event.target.value); | ||
|
||
if (tab) { | ||
router.push(tab.pathname); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<select | ||
className="block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-600 focus:outline-none focus:ring-indigo-600 sm:text-sm md:hidden" | ||
aria-label="Select a tab" | ||
value={pathname ?? ""} | ||
onChange={handleSelectChange} | ||
> | ||
{tabs.map(({ label, pathname }) => ( | ||
<option key={label} value={pathname}> | ||
{label} | ||
</option> | ||
))} | ||
</select> | ||
<nav className="hidden space-x-8 border-b border-gray-200 md:flex"> | ||
{tabs.map((tab) => ( | ||
<Link | ||
key={tab.pathname} | ||
href={tab.pathname} | ||
className={twMerge( | ||
"cursor-pointer whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium", | ||
tab.pathname === pathname | ||
? "border-indigo-600 text-indigo-600" | ||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700", | ||
)} | ||
> | ||
{tab.label} | ||
</Link> | ||
))} | ||
</nav> | ||
</> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be enum probably