Refine Integration #13
|
@ -108,7 +108,7 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem onClick={() => logout()}>
|
<DropdownMenuItem onClick={() => logout()}>
|
||||||
<ExitIcon className="mr-2" />
|
<ExitIcon className="mr-2" />
|
||||||
Log Out
|
Logout
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from "react";
|
||||||
|
import {Sdk} from "@lumeweb/portal-sdk";
|
||||||
|
|
||||||
|
export const SdkContext = React.createContext<
|
||||||
|
Partial<Sdk>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
export const SdkContextProvider: React.FC< {sdk: Sdk, children: React.ReactNode}> = ({sdk, children}) => {
|
||||||
|
return (
|
||||||
|
<SdkContext.Provider value={sdk}>
|
||||||
|
{children}
|
||||||
|
</SdkContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSdk(): Partial<Sdk>{
|
||||||
|
return React.useContext(SdkContext);
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import Uppy, {BasePlugin, DefaultPluginOptions} from '@uppy/core';
|
||||||
|
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
|
||||||
|
import {S5Client} from "@lumeweb/s5-js";
|
||||||
|
import {AxiosProgressEvent} from "axios";
|
||||||
|
|
||||||
|
|
||||||
|
interface UppyFileUploadOptions extends DefaultPluginOptions {
|
||||||
|
sdk: Sdk;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class UppyFileUpload extends BasePlugin {
|
||||||
|
private _sdk: Sdk;
|
||||||
|
|
||||||
|
constructor(uppy: Uppy, opts?: UppyFileUploadOptions) {
|
||||||
|
super(uppy, opts);
|
||||||
|
this.id = opts?.id || 'file-upload';
|
||||||
|
this.type = 'uploader';
|
||||||
|
this._sdk = opts?.sdk as Sdk;
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
this.uppy.addUploader(this.handleUpload.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleUpload(fileIDs: string[]) {
|
||||||
|
for (const fileID of fileIDs) {
|
||||||
|
const file = this.uppy.getFile(fileID);
|
||||||
|
if (!file) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (file.uploader !== 'file') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadLimit = await this._sdk.account().uploadLimit();
|
||||||
|
|
||||||
|
let data = file.data;
|
||||||
|
|
||||||
|
if (file.data instanceof Blob) {
|
||||||
|
data = new File([data], file.name, {type: file.type});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._sdk.protocols().get<S5Client>(PROTOCOL_S5).getSdk().uploadFile(data as File, {
|
||||||
|
largeFileSize: uploadLimit,
|
||||||
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
||||||
|
this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
|
||||||
|
uploader: this,
|
||||||
|
bytesUploaded: progressEvent.loaded,
|
||||||
|
bytesTotal: progressEvent.total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uppy.emit('upload-success', file, {uploadURL: null});
|
||||||
|
} catch (err) {
|
||||||
|
this.uppy.emit('upload-error', file, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,14 @@
|
||||||
import Uppy, { type State, debugLogger } from "@uppy/core"
|
import Uppy, {debugLogger, type State, UppyFile} from "@uppy/core"
|
||||||
|
|
||||||
import Tus from "@uppy/tus"
|
import Tus from "@uppy/tus"
|
||||||
import toArray from "@uppy/utils/lib/toArray"
|
import toArray from "@uppy/utils/lib/toArray"
|
||||||
|
|
||||||
import {
|
import {type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react"
|
||||||
type ChangeEvent,
|
import DropTarget, {type DropTargetOptions} from "./uppy-dropzone"
|
||||||
useCallback,
|
import {useSdk} from "~/components/lib/sdk-context.js";
|
||||||
useEffect,
|
import UppyFileUpload from "~/components/lib/uppy-file-upload.js";
|
||||||
useMemo,
|
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
|
||||||
useRef,
|
import {S5Client} from "@lumeweb/s5-js";
|
||||||
useState
|
|
||||||
} from "react"
|
|
||||||
import DropTarget, { type DropTargetOptions } from "./uppy-dropzone"
|
|
||||||
|
|
||||||
const LISTENING_EVENTS = [
|
const LISTENING_EVENTS = [
|
||||||
"upload",
|
"upload",
|
||||||
|
@ -23,12 +20,26 @@ const LISTENING_EVENTS = [
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export function useUppy({
|
export function useUppy({
|
||||||
uploader,
|
|
||||||
endpoint
|
endpoint
|
||||||
}: {
|
}: {
|
||||||
uploader: "tus"
|
|
||||||
endpoint: string
|
endpoint: string
|
||||||
}) {
|
}) {
|
||||||
|
const sdk = useSdk()
|
||||||
|
|
||||||
|
const [uploadLimit, setUploadLimit] = useState<number>(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getUploadLimit() {
|
||||||
|
try {
|
||||||
|
const limit = await sdk.account!().uploadLimit();
|
||||||
|
setUploadLimit(limit);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error occured while fetching upload limit', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getUploadLimit();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
|
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
|
||||||
const uppyInstance = useRef<Uppy>()
|
const uppyInstance = useRef<Uppy>()
|
||||||
|
@ -82,7 +93,29 @@ export function useUppy({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!targetRef) return
|
if (!targetRef) return
|
||||||
|
|
||||||
const uppy = new Uppy({ logger: debugLogger }).use(DropTarget, {
|
const tusPreprocessor = async (fileIDs: string[]) => {
|
||||||
|
for(const fileID of fileIDs) {
|
||||||
|
const file = uppyInstance.current?.getFile(fileID) as UppyFile
|
||||||
|
// @ts-ignore
|
||||||
|
if (file.uploader === "tus") {
|
||||||
|
uppyInstance.current?.setFileState(fileID, {
|
||||||
|
tus: await sdk.protocols!().get<S5Client>(PROTOCOL_S5).getSdk().getTusOptions(file.data as File)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uppy = new Uppy({
|
||||||
|
logger: debugLogger,
|
||||||
|
onBeforeUpload: (files) => {
|
||||||
|
for (const file of Object.entries(files)) {
|
||||||
|
// @ts-ignore
|
||||||
|
file[1].uploader = file[1].size > uploadLimit ? "tus" : "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}).use(DropTarget, {
|
||||||
target: targetRef
|
target: targetRef
|
||||||
} as DropTargetOptions)
|
} as DropTargetOptions)
|
||||||
|
|
||||||
|
@ -97,6 +130,25 @@ export function useUppy({
|
||||||
uppyInstance.current?.addFiles(files)
|
uppyInstance.current?.addFiles(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uppy.iteratePlugins((plugin) => {
|
||||||
|
uppy.removePlugin(plugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
uppy.use(UppyFileUpload, { sdk: sdk as Sdk })
|
||||||
|
|
||||||
|
let useTus = false;
|
||||||
|
|
||||||
|
uppyInstance.current?.getFiles().forEach((file) => {
|
||||||
|
if (file.size > uploadLimit) {
|
||||||
|
useTus = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (useTus) {
|
||||||
|
uppy.use(Tus, { endpoint: endpoint, limit: 6 })
|
||||||
|
uppy.addPreProcessor(tusPreprocessor)
|
||||||
|
}
|
||||||
|
|
||||||
// We clear the input after a file is selected, because otherwise
|
// We clear the input after a file is selected, because otherwise
|
||||||
// change event is not fired in Chrome and Safari when a file
|
// change event is not fired in Chrome and Safari when a file
|
||||||
// with the same name is selected.
|
// with the same name is selected.
|
||||||
|
@ -109,12 +161,7 @@ export function useUppy({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (uploader) {
|
|
||||||
case "tus":
|
|
||||||
uppy.use(Tus, { endpoint: endpoint, limit: 6 })
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
uppy.on("complete", (result) => {
|
uppy.on("complete", (result) => {
|
||||||
if (result.failed.length === 0) {
|
if (result.failed.length === 0) {
|
||||||
|
@ -148,7 +195,7 @@ export function useUppy({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setState("idle")
|
setState("idle")
|
||||||
}, [targetRef, endpoint, uploader])
|
}, [targetRef, endpoint, uploadLimit])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -33,10 +33,8 @@ export type Identity = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalAuthProvider implements RequiredAuthProvider {
|
export class PortalAuthProvider implements RequiredAuthProvider {
|
||||||
private sdk: Sdk;
|
|
||||||
|
|
||||||
constructor(apiUrl: string) {
|
constructor(apiUrl: string) {
|
||||||
this.sdk = Sdk.create(apiUrl);
|
this._sdk = Sdk.create(apiUrl);
|
||||||
|
|
||||||
const methods: Array<keyof AuthProvider> = [
|
const methods: Array<keyof AuthProvider> = [
|
||||||
'login',
|
'login',
|
||||||
|
@ -55,9 +53,18 @@ export class PortalAuthProvider implements RequiredAuthProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _sdk: Sdk;
|
||||||
|
|
||||||
|
get sdk(): Sdk {
|
||||||
|
return this._sdk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(apiUrl: string): AuthProvider {
|
||||||
|
return new PortalAuthProvider(apiUrl);
|
||||||
|
}
|
||||||
|
|
||||||
async login(params: AuthFormRequest): Promise<AuthActionResponse> {
|
async login(params: AuthFormRequest): Promise<AuthActionResponse> {
|
||||||
const cookies = new Cookies();
|
const ret = await this._sdk.account().login({
|
||||||
const ret = await this.sdk.account().login({
|
|
||||||
email: params.email,
|
email: params.email,
|
||||||
password: params.password,
|
password: params.password,
|
||||||
})
|
})
|
||||||
|
@ -65,11 +72,11 @@ export class PortalAuthProvider implements RequiredAuthProvider {
|
||||||
let redirectTo: string | undefined;
|
let redirectTo: string | undefined;
|
||||||
|
|
||||||
if (ret) {
|
if (ret) {
|
||||||
cookies.set('jwt', this.sdk.account().jwtToken, {path: '/'});
|
|
||||||
redirectTo = params.redirectTo;
|
redirectTo = params.redirectTo;
|
||||||
if (!redirectTo) {
|
if (!redirectTo) {
|
||||||
redirectTo = ret ? "/dashboard" : "/login";
|
redirectTo = ret ? "/dashboard" : "/login";
|
||||||
}
|
}
|
||||||
|
this._sdk.setAuthToken(this._sdk.account().jwtToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -79,38 +86,25 @@ export class PortalAuthProvider implements RequiredAuthProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(params: any): Promise<AuthActionResponse> {
|
async logout(params: any): Promise<AuthActionResponse> {
|
||||||
let ret = await this.sdk.account().logout();
|
let ret = await this._sdk.account().logout();
|
||||||
if (ret) {
|
|
||||||
const cookies = new Cookies();
|
|
||||||
cookies.remove('jwt');
|
|
||||||
}
|
|
||||||
return {success: ret, redirectTo: "/login"};
|
return {success: ret, redirectTo: "/login"};
|
||||||
}
|
}
|
||||||
|
|
||||||
async check(params?: any): Promise<CheckResponse> {
|
async check(params?: any): Promise<CheckResponse> {
|
||||||
const cookies = new Cookies();
|
this.maybeSetupAuth();
|
||||||
|
|
||||||
const jwtCookie = cookies.get('jwt');
|
const ret = await this._sdk.account().ping();
|
||||||
|
|
||||||
if (jwtCookie) {
|
|
||||||
this.sdk.setAuthToken(jwtCookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = await this.sdk.account().ping();
|
|
||||||
|
|
||||||
if (!ret) {
|
|
||||||
cookies.remove('jwt');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
|
return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
|
||||||
}
|
}
|
||||||
|
|
||||||
async onError(error: any): Promise<OnErrorResponse> {
|
async onError(error: any): Promise<OnErrorResponse> {
|
||||||
|
const cookies = new Cookies();
|
||||||
return {logout: true};
|
return {logout: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
|
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
|
||||||
const ret = await this.sdk.account().register({
|
const ret = await this._sdk.account().register({
|
||||||
email: params.email,
|
email: params.email,
|
||||||
password: params.password,
|
password: params.password,
|
||||||
first_name: params.firstName,
|
first_name: params.firstName,
|
||||||
|
@ -132,7 +126,8 @@ export class PortalAuthProvider implements RequiredAuthProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIdentity(params?: Identity): Promise<IdentityResponse> {
|
async getIdentity(params?: Identity): Promise<IdentityResponse> {
|
||||||
const ret = await this.sdk.account().info();
|
this.maybeSetupAuth();
|
||||||
|
const ret = await this._sdk.account().info();
|
||||||
|
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
return {identity: null};
|
return {identity: null};
|
||||||
|
@ -148,12 +143,16 @@ export class PortalAuthProvider implements RequiredAuthProvider {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create(apiUrl: string): AuthProvider {
|
maybeSetupAuth(): void {
|
||||||
return new PortalAuthProvider(apiUrl);
|
const cookies = new Cookies();
|
||||||
|
const jwtCookie = cookies.get('auth_token');
|
||||||
|
if (jwtCookie) {
|
||||||
|
this._sdk.setAuthToken(jwtCookie);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequiredAuthProvider extends AuthProvider {
|
export interface RequiredAuthProvider extends AuthProvider {
|
||||||
login: AuthProvider['login'];
|
login: AuthProvider['login'];
|
||||||
logout: AuthProvider['logout'];
|
logout: AuthProvider['logout'];
|
||||||
check: AuthProvider['check'];
|
check: AuthProvider['check'];
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
interface PinningStatus {
|
||||||
|
id: string;
|
||||||
|
progress: number;
|
||||||
|
status: 'inprogress' | 'completed' | 'stale';
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
|
||||||
|
class PinningProcess {
|
||||||
|
private static instances: Map<string, PinningStatus> = new Map();
|
||||||
|
|
||||||
|
static async pin(id: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
if (PinningProcess.instances.has(id)) {
|
||||||
|
return { success: false, message: "ID is already being processed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinningStatus: PinningStatus = { id, progress: 0, status: 'inprogress' };
|
||||||
|
PinningProcess.instances.set(id, pinningStatus);
|
||||||
|
|
||||||
|
// Simulate async progress
|
||||||
|
(async () => {
|
||||||
|
for (let progress = 1; progress <= 100; progress++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (500 - 100 + 1)) + 100)); // Simulate time passing with random duration between 100 and 500
|
||||||
|
pinningStatus.progress = progress;
|
||||||
|
if (progress === 100) {
|
||||||
|
pinningStatus.status = 'completed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { success: true, message: "Pinning process started" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static *pollProgress(id: string): Generator<PinningStatus | null, void, unknown> {
|
||||||
|
let status = PinningProcess.instances.get(id);
|
||||||
|
while (status && status.status !== 'completed') {
|
||||||
|
yield status;
|
||||||
|
status = PinningProcess.instances.get(id);
|
||||||
|
}
|
||||||
|
yield status ?? null; // Yield the final status, could be null if ID doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
// (async () => {
|
||||||
|
// const { success, message } = await PinningProcess.pin("123");
|
||||||
|
// console.log(message);
|
||||||
|
// if (success) {
|
||||||
|
// const progressGenerator = PinningProcess.pollProgress("123");
|
||||||
|
// let result = progressGenerator.next();
|
||||||
|
// while (!result.done) {
|
||||||
|
// console.log(result.value); // Log the progress
|
||||||
|
// result = progressGenerator.next();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })();
|
|
@ -15,6 +15,7 @@ import {Refine} from "@refinedev/core";
|
||||||
import {PortalAuthProvider} from "~/data/auth-provider.js";
|
import {PortalAuthProvider} from "~/data/auth-provider.js";
|
||||||
import routerProvider from "@refinedev/remix-router";
|
import routerProvider from "@refinedev/remix-router";
|
||||||
import { defaultProvider } from "./data/file-provider";
|
import { defaultProvider } from "./data/file-provider";
|
||||||
|
import {SdkContextProvider} from "~/components/lib/sdk-context.js";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [
|
||||||
{ rel: "stylesheet", href: stylesheet },
|
{ rel: "stylesheet", href: stylesheet },
|
||||||
|
@ -39,9 +40,10 @@ export function Layout({children}: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const auth = PortalAuthProvider.create("https://alpha.pinner.xyz")
|
||||||
return (
|
return (
|
||||||
<Refine
|
<Refine
|
||||||
authProvider={PortalAuthProvider.create("https://alpha.pinner.xyz")}
|
authProvider={auth}
|
||||||
routerProvider={routerProvider}
|
routerProvider={routerProvider}
|
||||||
dataProvider={defaultProvider}
|
dataProvider={defaultProvider}
|
||||||
resources={[
|
resources={[
|
||||||
|
@ -49,7 +51,9 @@ export default function App() {
|
||||||
{ name: 'users' }
|
{ name: 'users' }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
<SdkContextProvider sdk={(auth as PortalAuthProvider).sdk}>
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
|
</SdkContextProvider>
|
||||||
</Refine>
|
</Refine>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,23 @@
|
||||||
import { getFormProps, useForm } from "@conform-to/react";
|
import { getFormProps, useForm } from "@conform-to/react";
|
||||||
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
|
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
|
||||||
import { BaseKey, useGetIdentity, useUpdate, useUpdatePassword } from "@refinedev/core";
|
import {
|
||||||
|
BaseKey,
|
||||||
|
useGetIdentity,
|
||||||
|
useUpdate,
|
||||||
|
useUpdatePassword,
|
||||||
|
} from "@refinedev/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Field } from "~/components/forms";
|
import { Field } from "~/components/forms";
|
||||||
import { GeneralLayout } from "~/components/general-layout";
|
import { GeneralLayout } from "~/components/general-layout";
|
||||||
import { AddIcon, CloudIcon, CrownIcon } from "~/components/icons";
|
import { AddIcon, CloudIcon, CrownIcon } from "~/components/icons";
|
||||||
import { ManagementCard, ManagementCardAvatar, ManagementCardContent, ManagementCardFooter, ManagementCardTitle } from "~/components/management-card";
|
import {
|
||||||
|
ManagementCard,
|
||||||
|
ManagementCardAvatar,
|
||||||
|
ManagementCardContent,
|
||||||
|
ManagementCardFooter,
|
||||||
|
ManagementCardTitle,
|
||||||
|
} from "~/components/management-card";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -54,7 +65,9 @@ export default function MyAccount() {
|
||||||
{identity?.email}
|
{identity?.email}
|
||||||
</ManagementCardContent>
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, changeEmail: true })}>
|
<Button
|
||||||
|
className="h-12 gap-x-2"
|
||||||
|
onClick={() => setModal({ ...openModal, changeEmail: true })}>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
Change Email Address
|
Change Email Address
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -82,7 +95,9 @@ export default function MyAccount() {
|
||||||
<PasswordDots className="mt-6" />
|
<PasswordDots className="mt-6" />
|
||||||
</ManagementCardContent>
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, changePassword: true })}>
|
<Button
|
||||||
|
className="h-12 gap-x-2"
|
||||||
|
onClick={() => setModal({ ...openModal, changePassword: true })}>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
Change Password
|
Change Password
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -94,30 +109,22 @@ export default function MyAccount() {
|
||||||
Improve security by enabling 2FA.
|
Improve security by enabling 2FA.
|
||||||
</ManagementCardContent>
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, setupTwoFactor: true })}>
|
<Button
|
||||||
|
className="h-12 gap-x-2"
|
||||||
|
onClick={() => setModal({ ...openModal, setupTwoFactor: true })}>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
Enable Two-Factor Authorization
|
Enable Two-Factor Authorization
|
||||||
</Button>
|
</Button>
|
||||||
</ManagementCardFooter>
|
</ManagementCardFooter>
|
||||||
</ManagementCard>
|
</ManagementCard>
|
||||||
<ManagementCard>
|
|
||||||
<ManagementCardTitle>Backup Key</ManagementCardTitle>
|
|
||||||
<ManagementCardContent>
|
|
||||||
Never share this code with anyone.
|
|
||||||
</ManagementCardContent>
|
|
||||||
<ManagementCardFooter>
|
|
||||||
<Button className="h-12 gap-x-2">
|
|
||||||
<AddIcon />
|
|
||||||
Export Backup Key
|
|
||||||
</Button>
|
|
||||||
</ManagementCardFooter>
|
|
||||||
</ManagementCard>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-bold my-8">More</h2>
|
<h2 className="font-bold my-8">More</h2>
|
||||||
<div className="grid grid-cols-3 gap-x-8">
|
<div className="grid grid-cols-3 gap-x-8">
|
||||||
<ManagementCard variant="accent">
|
<ManagementCard variant="accent">
|
||||||
<ManagementCardTitle>Invite a Friend</ManagementCardTitle>
|
<ManagementCardTitle>Invite a Friend</ManagementCardTitle>
|
||||||
<ManagementCardContent>Get 1 GB per friend invited for free (max 5 GB).</ManagementCardContent>
|
<ManagementCardContent>
|
||||||
|
Get 1 GB per friend invited for free (max 5 GB).
|
||||||
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<Button variant="accent" className="h-12 gap-x-2">
|
<Button variant="accent" className="h-12 gap-x-2">
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
|
@ -127,7 +134,9 @@ export default function MyAccount() {
|
||||||
</ManagementCard>
|
</ManagementCard>
|
||||||
<ManagementCard>
|
<ManagementCard>
|
||||||
<ManagementCardTitle>Read our Resources</ManagementCardTitle>
|
<ManagementCardTitle>Read our Resources</ManagementCardTitle>
|
||||||
<ManagementCardContent>Navigate helpful articles or get assistance.</ManagementCardContent>
|
<ManagementCardContent>
|
||||||
|
Navigate helpful articles or get assistance.
|
||||||
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<Button className="h-12 gap-x-2">
|
<Button className="h-12 gap-x-2">
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
|
@ -137,7 +146,9 @@ export default function MyAccount() {
|
||||||
</ManagementCard>
|
</ManagementCard>
|
||||||
<ManagementCard>
|
<ManagementCard>
|
||||||
<ManagementCardTitle>Delete Account</ManagementCardTitle>
|
<ManagementCardTitle>Delete Account</ManagementCardTitle>
|
||||||
<ManagementCardContent>Once initiated, this action cannot be undone.</ManagementCardContent>
|
<ManagementCardContent>
|
||||||
|
Once initiated, this action cannot be undone.
|
||||||
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<Button className="h-12 gap-x-2" variant="destructive">
|
<Button className="h-12 gap-x-2" variant="destructive">
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
|
@ -170,12 +181,13 @@ export default function MyAccount() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangeEmailSchema = z.object({
|
const ChangeEmailSchema = z
|
||||||
|
.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
retypePassword: z.string(),
|
retypePassword: z.string(),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.password !== data.retypePassword) {
|
if (data.password !== data.retypePassword) {
|
||||||
return ctx.addIssue({
|
return ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
|
@ -184,7 +196,7 @@ const ChangeEmailSchema = z.object({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChangeEmailForm = ({
|
const ChangeEmailForm = ({
|
||||||
open,
|
open,
|
||||||
|
@ -195,7 +207,7 @@ const ChangeEmailForm = ({
|
||||||
setOpen: (value: boolean) => void;
|
setOpen: (value: boolean) => void;
|
||||||
currentValue: string;
|
currentValue: string;
|
||||||
}) => {
|
}) => {
|
||||||
const{ data: identity } = useGetIdentity<{ id: BaseKey }>();
|
const { data: identity } = useGetIdentity<{ id: BaseKey }>();
|
||||||
const { mutate: updateEmail } = useUpdate();
|
const { mutate: updateEmail } = useUpdate();
|
||||||
const [form, fields] = useForm({
|
const [form, fields] = useForm({
|
||||||
id: "login",
|
id: "login",
|
||||||
|
@ -210,13 +222,13 @@ const ChangeEmailForm = ({
|
||||||
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
|
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
|
||||||
console.log(identity);
|
console.log(identity);
|
||||||
updateEmail({
|
updateEmail({
|
||||||
resource: 'users',
|
resource: "users",
|
||||||
id: identity?.id || "",
|
id: identity?.id || "",
|
||||||
values: {
|
values: {
|
||||||
email: data.email.toString()
|
email: data.email.toString(),
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -255,12 +267,13 @@ const ChangeEmailForm = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChangePasswordSchema = z.object({
|
const ChangePasswordSchema = z
|
||||||
|
.object({
|
||||||
currentPassword: z.string().email(),
|
currentPassword: z.string().email(),
|
||||||
newPassword: z.string(),
|
newPassword: z.string(),
|
||||||
retypePassword: z.string(),
|
retypePassword: z.string(),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.newPassword !== data.retypePassword) {
|
if (data.newPassword !== data.retypePassword) {
|
||||||
return ctx.addIssue({
|
return ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
|
@ -269,7 +282,7 @@ const ChangePasswordSchema = z.object({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChangePasswordForm = ({
|
const ChangePasswordForm = ({
|
||||||
open,
|
open,
|
||||||
|
@ -292,9 +305,8 @@ const ChangePasswordForm = ({
|
||||||
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
|
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
|
||||||
|
|
||||||
updatePassword({
|
updatePassword({
|
||||||
password: data.newPassword.toString()
|
password: data.newPassword.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -390,6 +402,7 @@ const SetupTwoFactorDialog = ({
|
||||||
const PasswordDots = ({ className }: { className?: string }) => {
|
const PasswordDots = ({ className }: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
width="219"
|
width="219"
|
||||||
height="7"
|
height="7"
|
||||||
viewBox="0 0 219 7"
|
viewBox="0 0 219 7"
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"@conform-to/react": "^1.0.2",
|
"@conform-to/react": "^1.0.2",
|
||||||
"@conform-to/zod": "^1.0.2",
|
"@conform-to/zod": "^1.0.2",
|
||||||
"@fontsource-variable/manrope": "^5.0.19",
|
"@fontsource-variable/manrope": "^5.0.19",
|
||||||
"@lumeweb/portal-sdk": "0.0.0-20240314110748",
|
"@lumeweb/portal-sdk": "0.0.0-20240318183202",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
@ -40,10 +40,10 @@
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-cookie": "^7.1.0",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"universal-cookie": "^7.1.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -62,6 +62,7 @@
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"vite": "^5.1.0",
|
"vite": "^5.1.0",
|
||||||
|
"vite-plugin-node-polyfills": "^0.21.0",
|
||||||
"vite-tsconfig-paths": "^4.2.1"
|
"vite-tsconfig-paths": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
diff --git a/node_modules/@uppy/tus/lib/index.js b/node_modules/@uppy/tus/lib/index.js
|
||||||
|
index 1e0a1bb..ba95bb5 100644
|
||||||
|
--- a/node_modules/@uppy/tus/lib/index.js
|
||||||
|
+++ b/node_modules/@uppy/tus/lib/index.js
|
||||||
|
@@ -506,7 +506,7 @@ function _getCompanionClientArgs2(file) {
|
||||||
|
}
|
||||||
|
async function _uploadFiles2(files) {
|
||||||
|
const filesFiltered = filterNonFailedFiles(files);
|
||||||
|
- const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered);
|
||||||
|
+ const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered).filter(file => file?.uploader == 'tus');
|
||||||
|
this.uppy.emit('upload-start', filesToEmit);
|
||||||
|
await Promise.allSettled(filesFiltered.map(file => {
|
||||||
|
if (file.isRemote) {
|
|
@ -1,6 +1,7 @@
|
||||||
import { vitePlugin as remix } from "@remix-run/dev"
|
import {vitePlugin as remix} from "@remix-run/dev"
|
||||||
import { defineConfig } from "vite"
|
import {defineConfig} from "vite"
|
||||||
import tsconfigPaths from "vite-tsconfig-paths"
|
import tsconfigPaths from "vite-tsconfig-paths"
|
||||||
|
import {nodePolyfills} from 'vite-plugin-node-polyfills'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -8,8 +9,12 @@ export default defineConfig({
|
||||||
ssr: false,
|
ssr: false,
|
||||||
ignoredRouteFiles: ["**/*.css"]
|
ignoredRouteFiles: ["**/*.css"]
|
||||||
}),
|
}),
|
||||||
tsconfigPaths()
|
tsconfigPaths(),
|
||||||
|
nodePolyfills({protocolImports: false}),
|
||||||
],
|
],
|
||||||
|
build: {
|
||||||
|
minify: false
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
fs: {
|
fs: {
|
||||||
// Restrict files that could be served by Vite's dev server. Accessing
|
// Restrict files that could be served by Vite's dev server. Accessing
|
||||||
|
|
Loading…
Reference in New Issue