Skip to content
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

Closed
wants to merge 13 commits into from
Closed
39 changes: 13 additions & 26 deletions src/components/common/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,29 @@
import { forwardRef } from "react";
import { twMerge } from "tailwind-merge";

const variants = [
{
name: "primary",
className:
"border-transparent bg-indigo-600 text-white enabled:hover:bg-indigo-700",
},
{
name: "secondary",
className:
"border-gray-300 bg-white text-gray-700 enabled:hover:bg-gray-50",
},
{
name: "danger",
className:
"border-transparent bg-red-600 text-white enabled:hover:bg-red-700 focus:ring-red-500",
},
] as const;
export enum BUTTON_VARIANT {
PRIMARY = "border-transparent bg-indigo-600 text-white enabled:hover:bg-indigo-700",
SECONDARY = "border-gray-300 bg-white text-gray-700 enabled:hover:bg-gray-50",
DANGER = "border-transparent bg-red-600 text-white enabled:hover:bg-red-700 focus:ring-red-500",
TEXT = "rounded-md border-none bg-gray-100 font-medium text-indigo-600 shadow-none hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-100",
}

export const Button = forwardRef<
HTMLButtonElement,
JSX.IntrinsicElements["button"] & {
variant?: typeof variants[number]["name"];
variant?: BUTTON_VARIANT;
}
>(({ variant = "primary", children, ...props }, ref) => {
>(({ variant = BUTTON_VARIANT.PRIMARY, className, ...props }, ref) => {
return (
<button
{...props}
ref={ref}
className={twMerge(
"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50",
variants.find(({ name }) => variant === name)?.className,
props.className,
variant,
className,
)}
>
{children}
</button>
ref={ref}
{...props}
/>
);
});

Expand Down
32 changes: 32 additions & 0 deletions src/components/dashboard/heading/Heading.tsx
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 = {
Copy link
Owner

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

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}
/>
);
};
26 changes: 26 additions & 0 deletions src/components/dashboard/settingsLayout/SettingsLayout.tsx
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",
},
];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make /settings folder, move all components related to it there (without Settings prefix) and move something like this to consts file in /config


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>
);
70 changes: 70 additions & 0 deletions src/components/dashboard/settingsRow/RowForm.tsx
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>
);
};
65 changes: 65 additions & 0 deletions src/components/dashboard/settingsRow/SettingsRow.tsx
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";
Copy link
Owner

Choose a reason for hiding this comment

The 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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content is optional?

readonly onChange: (content: string) => void;
}

export const SettingsRow = ({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put this component in /settings folder

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>
);
};
6 changes: 3 additions & 3 deletions src/components/dashboard/sync/SyncsList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memo } from "react";

import { Button } from "../../common/Button";
import { Button, BUTTON_VARIANT } from "../../common/Button";

import { SyncItem } from "./SyncItem";

Expand Down Expand Up @@ -111,15 +111,15 @@ export const SyncsList = memo<SyncsListProps>(
<div className="flex flex-1 justify-between gap-2 sm:justify-end">
<Button
disabled={page === 1}
variant="secondary"
variant={BUTTON_VARIANT.SECONDARY}
onClick={() => onPageChange(page - 1)}
>
Previous
</Button>

<Button
disabled={(total ?? 0) < perPage * page}
variant="secondary"
variant={BUTTON_VARIANT.SECONDARY}
onClick={() => onPageChange(page + 1)}
>
Next
Expand Down
60 changes: 60 additions & 0 deletions src/components/dashboard/tabs/Tabs.tsx
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>
</>
);
};
8 changes: 6 additions & 2 deletions src/components/modal/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";

import { Button } from "../common/Button";
import { Button, BUTTON_VARIANT } from "../common/Button";

import { BaseModal } from "./BaseModal";

Expand All @@ -27,7 +27,11 @@ export const ConfirmModal = ({
<h3 className="text-bold text-3xl text-gray-600">Are you sure?</h3>
<p className="text-center text-sm text-gray-500">{content}</p>
<div className="flex gap-4">
<Button type="button" variant="secondary" onClick={onClose}>
<Button
type="button"
variant={BUTTON_VARIANT.SECONDARY}
onClick={onClose}
>
Cancel
</Button>
<Button
Expand Down
8 changes: 6 additions & 2 deletions src/components/modal/FormModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button } from "../common/Button";
import { Button, BUTTON_VARIANT } from "../common/Button";

import { BaseModal } from "./BaseModal";

Expand Down Expand Up @@ -40,7 +40,11 @@ export const FormModal = ({
</div>
</div>
<div className="flex justify-end gap-2 bg-gray-50 px-6 py-3">
<Button type="button" variant="secondary" onClick={onClose}>
<Button
type="button"
variant={BUTTON_VARIANT.SECONDARY}
onClick={onClose}
>
Cancel
</Button>
<Button type="submit">{submitText}</Button>
Expand Down
4 changes: 2 additions & 2 deletions src/components/tile/deviceTile/DeviceTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { memo } from "react";
import RemarkableIcon from "public/svg/remarkable.svg";

import { onPromise } from "../../../utils/functions";
import { Button } from "../../common/Button";
import { Button, BUTTON_VARIANT } from "../../common/Button";

import type { Device } from "@prisma/client";

Expand Down Expand Up @@ -32,7 +32,7 @@ export const DeviceTile = memo<DeviceTileProps>(({ device, onDelete }) => {
<Button
onClick={onPromise(onDelete)}
className="bottom-10 right-10 mt-4 md:absolute lg:right-8 lg:bottom-8 xl:right-12 xl:bottom-12"
variant="danger"
variant={BUTTON_VARIANT.DANGER}
>
Delete device
</Button>
Expand Down
Loading