fix: small fixes

This commit is contained in:
Juan Di Toro 2024-03-22 18:53:39 +01:00
parent e1ca45f1f1
commit c9de506c56
5 changed files with 331 additions and 274 deletions

View File

@ -1,51 +1,62 @@
import { useMutation } from "@tanstack/react-query"; import { useNotification } from "@refinedev/core";
import { useContext } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useCallback, useContext } from "react";
import { PinningProcess } from "~/data/pinning"; import { PinningProcess } from "~/data/pinning";
import { PinningContext } from "~/providers/PinningProvider"; import { PinningContext } from "~/providers/PinningProvider";
// TODO: Adapt to real API
export const usePinning = () => { export const usePinning = () => {
const queryClient = useQueryClient();
const context = useContext(PinningContext); const context = useContext(PinningContext);
const { open } = useNotification();
const { mutate } = useMutation({ const { mutate: pinMutation } = useMutation({
mutationKey: ["pin-progress"], mutationKey: ["pin-mutation"],
mutationFn: async (variables: { cid: string, type: "pin" | "unpin" }) => { mutationFn: async (variables: { cid: string }) => {
const { cid, type } = variables; const { cid } = variables;
switch (type) { const response = await PinningProcess.pin(cid);
case "pin": {
const response = await PinningProcess.pin(cid);
if (!response.success) { if (!response.success) {
open?.({ open?.({
type: "destructive", type: "error",
message: "Erorr pinning " + cid, message: `Error pinning ${cid}`,
description: response.message, description: response.message,
}); });
}
break;
}
case "unpin": {
const response = await PinningProcess.unpin(cid);
if (!response.success) {
open?.({
type: "destructive",
message: "Erorr removing " + cid,
description: response.message,
});
}
break;
}
} }
context.queryClient.invalidateQueries({ queryKey: ["pin-progress"] }) queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
} },
}); });
const { mutate: unpinMutation } = useMutation({
mutationKey: ["unpin-mutation"],
mutationFn: async (variables: { cid: string }) => {
const { cid } = variables;
const response = await PinningProcess.unpin(cid);
if (!response.success) {
open?.({
type: "error",
message: `Error removing ${cid}`,
description: response.message,
});
}
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
},
});
const bulkPin = useCallback(
(cids: string[]) => {
for (const cid of cids) {
pinMutation({ cid });
}
},
[pinMutation],
);
return { return {
...context.query, ...context.query,
mutate pin: pinMutation,
unpin: unpinMutation,
bulkPin,
}; };
}; };

View File

@ -1,11 +1,11 @@
import { import {
QueryClient, type QueryClient,
UseQueryResult, type UseQueryResult,
useQuery, useQuery,
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { PinningProcess, PinningStatus } from "~/data/pinning"; import { PinningProcess, type PinningStatus } from "~/data/pinning";
export interface IPinningData { export interface IPinningData {
cid: string; cid: string;

View File

@ -1,79 +1,82 @@
import {Links, Meta, Outlet, Scripts, ScrollRestoration,} from "@remix-run/react"; import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import stylesheet from "./tailwind.css?url"; import stylesheet from "./tailwind.css?url";
import type {LinksFunction} from "@remix-run/node"; import type { LinksFunction } from "@remix-run/node";
// Supports weights 200-800 // Supports weights 200-800
import '@fontsource-variable/manrope'; import "@fontsource-variable/manrope";
import {Refine} from "@refinedev/core"; import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/remix-router"; import routerProvider from "@refinedev/remix-router";
import {notificationProvider} from "~/data/notification-provider"; import { notificationProvider } from "~/data/notification-provider";
import {SdkContextProvider, useSdk} from "~/components/lib/sdk-context"; import { SdkContextProvider, useSdk } from "~/components/lib/sdk-context";
import {Toaster} from "~/components/ui/toaster"; import { Toaster } from "~/components/ui/toaster";
import {getProviders} from "~/data/providers.js"; import { getProviders } from "~/data/providers.js";
import {Sdk} from "@lumeweb/portal-sdk"; import { Sdk } from "@lumeweb/portal-sdk";
import resources from "~/data/resources.js"; import resources from "~/data/resources.js";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {useMemo} from "react"; import { useMemo } from "react";
export const links: LinksFunction = () => [ export const links: LinksFunction = () => [
{rel: "stylesheet", href: stylesheet}, { rel: "stylesheet", href: stylesheet },
]; ];
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export function Layout({children}: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<meta charSet="utf-8"/> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta/> <Meta />
<Links/> <Links />
</head> </head>
<body className="overflow-hidden"> <body className="overflow-hidden">
{children} {children}
<Toaster/> <Toaster />
<ScrollRestoration/> <ScrollRestoration />
<Scripts/> <Scripts />
</body> </body>
</html> </html>
); );
} }
function App() { function App() {
const sdk = useSdk(); const sdk = useSdk();
const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]); const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Refine <Refine
authProvider={providers.auth} authProvider={providers.auth}
routerProvider={routerProvider} routerProvider={routerProvider}
notificationProvider={notificationProvider} notificationProvider={notificationProvider}
dataProvider={{ dataProvider={{
default: providers.default, default: providers.default,
files: providers.files files: providers.files,
}} }}
resources={resources} resources={resources}
options={{disableTelemetry: true}} options={{ disableTelemetry: true }}>
> <Outlet />
<SdkContextProvider sdk={sdk}> </Refine>
<Outlet/> </QueryClientProvider>
</SdkContextProvider> );
</Refine>
</QueryClientProvider>
);
} }
export default function Root() { export default function Root() {
const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL) const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL);
return ( return (
<SdkContextProvider sdk={sdk}> <SdkContextProvider sdk={sdk}>
<App/> <App />
</SdkContextProvider> </SdkContextProvider>
); );
} }
export function HydrateFallback() { export function HydrateFallback() {
return <p>Loading...</p>; return <p>Loading...</p>;
} }

View File

@ -3,11 +3,11 @@ import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { DialogClose } from "@radix-ui/react-dialog"; import { DialogClose } from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons"; import { Cross2Icon } from "@radix-ui/react-icons";
import { import {
Authenticated, Authenticated,
type BaseKey, type BaseKey,
useGetIdentity, useGetIdentity,
useUpdate, useUpdate,
useUpdatePassword, useUpdatePassword,
} from "@refinedev/core"; } from "@refinedev/core";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { z } from "zod"; import { z } from "zod";
@ -41,7 +41,7 @@ import { Input } from "~/components/ui/input";
import { UsageCard } from "~/components/usage-card"; import { UsageCard } from "~/components/usage-card";
import QRImg from "~/images/QR.png"; import QRImg from "~/images/QR.png";
import type {UpdatePasswordFormRequest} from "~/data/auth-provider"; import type { UpdatePasswordFormRequest } from "~/data/auth-provider";
export default function MyAccount() { export default function MyAccount() {
const { data: identity } = useGetIdentity<{ email: string }>(); const { data: identity } = useGetIdentity<{ email: string }>();
@ -54,167 +54,175 @@ export default function MyAccount() {
}); });
return ( return (
<Authenticated key="account" v3LegacyAuthProviderCompatible> <Authenticated key="account" v3LegacyAuthProviderCompatible>
<GeneralLayout> <GeneralLayout>
<h1 className="text-lg font-bold mb-4">My Account</h1> <h1 className="text-lg font-bold mb-4">My Account</h1>
<Dialog <Dialog
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setModal({ setModal({
changeEmail: false, changeEmail: false,
changePassword: false, changePassword: false,
setupTwoFactor: false, setupTwoFactor: false,
changeAvatar: false, changeAvatar: false,
}); });
} }
}}> }}>
<UsageCard <UsageCard
label="Usage" label="Usage"
currentUsage={2} currentUsage={2}
monthlyUsage={10} monthlyUsage={10}
icon={<CloudIcon className="text-ring" />} icon={<CloudIcon className="text-ring" />}
button={ button={
<Button variant="accent" className="gap-x-2 h-12"> <Button variant="accent" className="gap-x-2 h-12">
<AddIcon />
Upgrade to Premium
</Button>
}
/>
<h2 className="font-bold my-8">Account Management</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard>
<ManagementCardAvatar
button={
<DialogTrigger asChild className="absolute bottom-0 right-0 z-50">
<Button
onClick={() => setModal({ ...openModal, changeAvatar: true })}
variant="outline"
className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
<EditIcon />
</Button>
</DialogTrigger>
}
/>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold">
{identity?.email}
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, changeEmail: true })}>
<AddIcon />
Change Email Address
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Account Type</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold flex gap-x-2">
Lite Premium Account
<CrownIcon />
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon /> <AddIcon />
Upgrade to Premium Upgrade to Premium
</Button> </Button>
</ManagementCardFooter> }
</ManagementCard> />
</div> <h2 className="font-bold my-8">Account Management</h2>
<h2 className="font-bold my-8">Security</h2> <div className="grid grid-cols-3 gap-x-8">
<div className="grid grid-cols-3 gap-x-8"> <ManagementCard>
<ManagementCard> <ManagementCardAvatar
<ManagementCardTitle>Password</ManagementCardTitle> button={
<ManagementCardContent> <DialogTrigger
<PasswordDots className="mt-6" /> asChild
</ManagementCardContent> className="absolute bottom-0 right-0 z-50">
<ManagementCardFooter> <Button
<DialogTrigger asChild> onClick={() =>
<Button setModal({ ...openModal, changeAvatar: true })
className="h-12 gap-x-2" }
onClick={() => variant="outline"
setModal({ ...openModal, changePassword: true }) className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
}> <EditIcon />
</Button>
</DialogTrigger>
}
/>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold">
{identity?.email}
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() =>
setModal({ ...openModal, changeEmail: true })
}>
<AddIcon />
Change Email Address
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Account Type</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold flex gap-x-2">
Lite Premium Account
<CrownIcon />
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon /> <AddIcon />
Change Password Upgrade to Premium
</Button> </Button>
</DialogTrigger> </ManagementCardFooter>
</ManagementCardFooter> </ManagementCard>
</ManagementCard> </div>
<ManagementCard> <h2 className="font-bold my-8">Security</h2>
<ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle> <div className="grid grid-cols-3 gap-x-8">
<ManagementCardContent> <ManagementCard>
Improve security by enabling 2FA. <ManagementCardTitle>Password</ManagementCardTitle>
</ManagementCardContent> <ManagementCardContent>
<ManagementCardFooter> <PasswordDots className="mt-6" />
<DialogTrigger asChild> </ManagementCardContent>
<Button <ManagementCardFooter>
className="h-12 gap-x-2" <DialogTrigger asChild>
onClick={() => <Button
setModal({ ...openModal, setupTwoFactor: true }) className="h-12 gap-x-2"
}> onClick={() =>
setModal({ ...openModal, changePassword: true })
}>
<AddIcon />
Change Password
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>
Two-Factor Authentication
</ManagementCardTitle>
<ManagementCardContent>
Improve security by enabling 2FA.
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() =>
setModal({ ...openModal, setupTwoFactor: true })
}>
<AddIcon />
Enable Two-Factor Authorization
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
</div>
<h2 className="font-bold my-8">More</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard variant="accent">
<ManagementCardTitle>Invite a Friend</ManagementCardTitle>
<ManagementCardContent>
Get 1 GB per friend invited for free (max 5 GB).
</ManagementCardContent>
<ManagementCardFooter>
<Button variant="accent" className="h-12 gap-x-2">
<AddIcon /> <AddIcon />
Enable Two-Factor Authorization Send Invitation
</Button> </Button>
</DialogTrigger> </ManagementCardFooter>
</ManagementCardFooter> </ManagementCard>
</ManagementCard> <ManagementCard>
</div> <ManagementCardTitle>Read our Resources</ManagementCardTitle>
<h2 className="font-bold my-8">More</h2> <ManagementCardContent>
<div className="grid grid-cols-3 gap-x-8"> Navigate helpful articles or get assistance.
<ManagementCard variant="accent"> </ManagementCardContent>
<ManagementCardTitle>Invite a Friend</ManagementCardTitle> <ManagementCardFooter>
<ManagementCardContent> <Button className="h-12 gap-x-2">
Get 1 GB per friend invited for free (max 5 GB). <AddIcon />
</ManagementCardContent> Open Support Centre
<ManagementCardFooter> </Button>
<Button variant="accent" className="h-12 gap-x-2"> </ManagementCardFooter>
<AddIcon /> </ManagementCard>
Send Invitation <ManagementCard>
</Button> <ManagementCardTitle>Delete Account</ManagementCardTitle>
</ManagementCardFooter> <ManagementCardContent>
</ManagementCard> Once initiated, this action cannot be undone.
<ManagementCard> </ManagementCardContent>
<ManagementCardTitle>Read our Resources</ManagementCardTitle> <ManagementCardFooter>
<ManagementCardContent> <Button className="h-12 gap-x-2" variant="destructive">
Navigate helpful articles or get assistance. <AddIcon />
</ManagementCardContent> Delete my Account
<ManagementCardFooter> </Button>
<Button className="h-12 gap-x-2"> </ManagementCardFooter>
<AddIcon /> </ManagementCard>
Open Support Centre </div>
</Button> <DialogContent>
</ManagementCardFooter> {openModal.changeAvatar && <ChangeAvatarForm />}
</ManagementCard> {openModal.changeEmail && (
<ManagementCard> <ChangeEmailForm currentValue={identity?.email || ""} />
<ManagementCardTitle>Delete Account</ManagementCardTitle> )}
<ManagementCardContent> {openModal.changePassword && <ChangePasswordForm />}
Once initiated, this action cannot be undone. {openModal.setupTwoFactor && <SetupTwoFactorDialog />}
</ManagementCardContent> </DialogContent>
<ManagementCardFooter> </Dialog>
<Button className="h-12 gap-x-2" variant="destructive"> </GeneralLayout>
<AddIcon /> </Authenticated>
Delete my Account
</Button>
</ManagementCardFooter>
</ManagementCard>
</div>
<DialogContent>
{openModal.changeAvatar && <ChangeAvatarForm />}
{openModal.changeEmail && (
<ChangeEmailForm currentValue={identity?.email || ""} />
)}
{openModal.changePassword && <ChangePasswordForm />}
{openModal.setupTwoFactor && <SetupTwoFactorDialog />}
</DialogContent>
</Dialog>
</GeneralLayout>
</Authenticated>
); );
} }
@ -252,7 +260,7 @@ const ChangeEmailForm = ({ currentValue }: { currentValue: string }) => {
console.log(identity); console.log(identity);
updateEmail({ updateEmail({
resource: "account", resource: "account",
id: "", id: "",
values: { values: {
email: data.email.toString(), email: data.email.toString(),
password: data.password.toString(), password: data.password.toString(),
@ -313,7 +321,8 @@ const ChangePasswordSchema = z
}); });
const ChangePasswordForm = () => { const ChangePasswordForm = () => {
const { mutate: updatePassword } = useUpdatePassword<UpdatePasswordFormRequest>(); const { mutate: updatePassword } =
useUpdatePassword<UpdatePasswordFormRequest>();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "login", id: "login",
constraint: getZodConstraint(ChangePasswordSchema), constraint: getZodConstraint(ChangePasswordSchema),
@ -327,8 +336,8 @@ const ChangePasswordForm = () => {
const data = Object.fromEntries(new FormData(e.currentTarget).entries()); const data = Object.fromEntries(new FormData(e.currentTarget).entries());
updatePassword({ updatePassword({
currentPassword: data.currentPassword.toString(), currentPassword: data.currentPassword.toString(),
password: data.newPassword.toString(), password: data.newPassword.toString(),
}); });
}, },
}); });

View File

@ -14,6 +14,10 @@ import {
DialogTrigger, DialogTrigger,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { Field } from "~/components/forms"; import { Field } from "~/components/forms";
import { z } from "zod";
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { usePinning } from "~/hooks/usePinning";
export default function FileManager() { export default function FileManager() {
return ( return (
@ -72,25 +76,55 @@ export default function FileManager() {
dataProviderName="files" dataProviderName="files"
/> />
<DialogContent> <DialogContent>
<DialogHeader> <PinFilesForm />
<DialogTitle>Pin Content</DialogTitle>
</DialogHeader>
<form action="" className="w-full flex flex-col gap-y-4">
<Field
inputProps={{
name: "cids",
placeholder: "Comma separated CIDs",
}}
labelProps={{ htmlFor: "cids", children: "Content to Pin" }}
/>
<Button type="submit" className="w-full">
Pin Content
</Button>
</form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</GeneralLayout> </GeneralLayout>
</Authenticated> </Authenticated>
); );
} }
const PinFilesSchema = z.object({
cids: z.string().transform((value) => value.split(",")),
});
const PinFilesForm = () => {
const { bulkPin } = usePinning();
const [form, fields] = useForm({
id: "pin-files",
constraint: getZodConstraint(PinFilesSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: PinFilesSchema });
},
shouldValidate: "onSubmit",
onSubmit(e, { submission }) {
if (submission?.status === "success") {
const value = submission.value;
bulkPin(value.cids);
}
},
});
return (
<>
<DialogHeader>
<DialogTitle>Pin Content</DialogTitle>
</DialogHeader>
<form {...getFormProps(form)} className="w-full flex flex-col gap-y-4">
<Field
inputProps={{
name: fields.cids.name,
placeholder: "Comma separated CIDs",
}}
labelProps={{ htmlFor: "cids", children: "Content to Pin" }}
errors={fields.cids.errors}
/>
<Button type="submit" className="w-full">
Pin Content
</Button>
</form>
</>
);
};