Skip to content

Commit

Permalink
feat(console): implement custom ui assets upload component
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao committed Jul 17, 2024
1 parent 0144b74 commit cb605e8
Show file tree
Hide file tree
Showing 21 changed files with 468 additions and 69 deletions.
102 changes: 102 additions & 0 deletions packages/console/src/assets/images/blur-preview.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@use '@/scss/underscore' as _;

.placeholder {
display: flex;
align-items: center;
padding: _.unit(5) _.unit(5) _.unit(5) _.unit(4);
border: 1px solid var(--color-border);
border-radius: 12px;
gap: _.unit(4);
position: relative;
overflow: hidden;

.main {
display: flex;
flex-direction: column;
gap: _.unit(0.5);
flex: 1;

.name {
font: var(--font-label-2);
color: var(--color-text-primary);
}

.secondaryInfo {
display: flex;
align-items: center;
gap: _.unit(3);
}

.info {
font: var(--font-body-3);
color: var(--color-text-secondary);
}

.error {
font: var(--font-body-3);
color: var(--color-error);
}
}

.icon {
width: 40px;
height: 40px;
}

// display a fake progress bar on the bottom with animations
.progressBar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background-color: var(--color-primary);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s;
}

&.hasError {
border-color: var(--color-error);
}
}
89 changes: 89 additions & 0 deletions packages/console/src/components/CustomUiAssetsUploader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { type CustomUiAssets, maxUploadFileSize, type AllowedUploadMimeType } from '@logto/schemas';
import { format } from 'date-fns/fp';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';

import DeleteIcon from '@/assets/icons/delete.svg';
import IconButton from '@/ds-components/IconButton';
import FileUploader from '@/ds-components/Uploader/FileUploader';
import { formatBytes } from '@/utils/uploader';

import FileIcon from '../FileIcon';

import * as styles from './index.module.scss';

type Props = {
readonly value?: CustomUiAssets;
readonly onChange: (value: CustomUiAssets) => void;
};

const allowedMimeTypes: AllowedUploadMimeType[] = ['application/zip'];

function CustomUiAssetsUploader({ value, onChange }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [file, setFile] = useState<File>();
const [error, setError] = useState<string>();
const showUploader = !value?.id && !file && !error;

const onComplete = useCallback(
(id: string) => {
setFile(undefined);
onChange({ id, createdAt: Date.now() });
},
[onChange]
);

const onErrorChange = useCallback(
(errorMessage?: string, files?: File[]) => {
if (errorMessage) {
setError(errorMessage);
}
if (files?.length) {
setFile(files[0]);
}
},
[setError, setFile]
);

if (showUploader) {
return (
<FileUploader<{ customUiAssetId: string }>
allowedMimeTypes={allowedMimeTypes}
maxSize={maxUploadFileSize}
uploadUrl="api/sign-in-exp/default/custom-ui-assets"
onCompleted={({ customUiAssetId }) => {
onComplete(customUiAssetId);
}}
onUploadErrorChange={onErrorChange}
/>
);
}

return (
<div className={styles.placeholder}>
<FileIcon />
<div className={styles.main}>
<div className={styles.name}>{file?.name ?? t('sign_in_exp.custom_ui.title')}</div>
<div className={styles.secondaryInfo}>
{!!value?.createdAt && (
<span className={styles.info}>{format('yyyy/MM/dd HH:mm')(value.createdAt)}</span>
)}
{file && <span className={styles.info}>{formatBytes(file.size)}</span>}
{error && <span className={styles.error}>{error}</span>}
</div>
</div>
<IconButton
onClick={() => {
setFile(undefined);
setError(undefined);
onChange({ id: '', createdAt: 0 });
}}
>
<DeleteIcon />
</IconButton>
{file && <div className={styles.progressBar} />}
</div>
);
}

export default CustomUiAssetsUploader;
20 changes: 20 additions & 0 deletions packages/console/src/components/FileIcon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Theme } from '@logto/schemas';
import { type ReactNode } from 'react';

import FileIconDark from '@/assets/icons/file-icon-dark.svg';
import FileIconLight from '@/assets/icons/file-icon.svg';
import useTheme from '@/hooks/use-theme';

const themeToRoleIcon = Object.freeze({
[Theme.Light]: <FileIconLight />,
[Theme.Dark]: <FileIconDark />,
} satisfies Record<Theme, ReactNode>);

/** Render a role icon according to the current theme. */
const FileIcon = () => {
const theme = useTheme();

return themeToRoleIcon[theme];
};

export default FileIcon;
4 changes: 3 additions & 1 deletion packages/console/src/components/ImageInputs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ function ImageInputs<FormContext extends FieldValues>({
actionDescription={t(`sign_in_exp.branding.with_${field.theme}`, {
value: t(`sign_in_exp.branding_uploads.${field.type}.title`),
})}
onCompleted={onChange}
onCompleted={({ url }) => {
onChange(url);
}}
// Noop fallback should not be necessary, but for TypeScript to be happy
onUploadErrorChange={uploadErrorChangeHandlers[field.name] ?? noop}
onDelete={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,28 @@
}
}
}

&.disabled {
background: url('raw:../../assets/images/blur-preview.svg') 0 0 / 100% auto no-repeat;
}

.placeholder {
width: 100%;
height: calc(_screenSize.$web-height + _.unit(20));
padding: _.unit(10);
backdrop-filter: blur(25px);
display: flex;
flex-direction: column;
color: var(--color-static-white);

.title {
font: var(--font-label-2);
}

.description {
margin-top: _.unit(1.5);
font: var(--font-body-2);
white-space: pre-wrap;
}
}
}
74 changes: 54 additions & 20 deletions packages/console/src/components/SignInExperiencePreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import type { ConnectorMetadata, SignInExperience, ConnectorResponse } from '@lo
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import { format } from 'date-fns';
import { useContext, useRef, useMemo, useCallback, useEffect, useState } from 'react';
import {
useContext,
useRef,
useMemo,
useCallback,
useEffect,
useState,
type ReactNode,
} from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';

Expand All @@ -28,6 +36,16 @@ type Props = {
* the `AppDataContext` will be used.
*/
readonly endpoint?: URL;
/**
* Whether the preview is disabled. If `true`, the preview will be disabled and a placeholder will
* be shown instead. Defaults to `false`.
*/
// eslint-disable-next-line react/boolean-prop-naming
readonly disabled?: boolean;
/**
* The placeholder to show when the preview is disabled.
*/
readonly disabledPlaceholder?: ReactNode;
};

function SignInExperiencePreview({
Expand All @@ -36,6 +54,8 @@ function SignInExperiencePreview({
language = 'en',
signInExperience,
endpoint: endpointInput,
disabled = false,
disabledPlaceholder,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });

Expand Down Expand Up @@ -97,14 +117,18 @@ function SignInExperiencePreview({
}, []);

useEffect(() => {
if (disabled) {
setIframeLoaded(false);
return;
}
const iframe = previewRef.current;

iframe?.addEventListener('load', iframeOnLoadEventHandler);

return () => {
iframe?.removeEventListener('load', iframeOnLoadEventHandler);
};
}, [iframeLoaded, iframeOnLoadEventHandler]);
}, [iframeLoaded, disabled, iframeOnLoadEventHandler]);

useEffect(() => {
if (!iframeLoaded) {
Expand All @@ -122,7 +146,8 @@ function SignInExperiencePreview({
<div
className={classNames(
styles.preview,
platform === PreviewPlatform.DesktopWeb ? styles.web : styles.mobile
platform === PreviewPlatform.DesktopWeb ? styles.web : styles.mobile,
disabled && styles.disabled
)}
style={conditional(
platform === PreviewPlatform.DesktopWeb && {
Expand All @@ -131,24 +156,33 @@ function SignInExperiencePreview({
}
)}
>
<div className={styles.deviceWrapper}>
<div className={classNames(styles.device, styles[String(mode)])}>
{platform !== PreviewPlatform.DesktopWeb && (
<div className={styles.topBar}>
<div className={styles.time}>{format(Date.now(), 'HH:mm')}</div>
<PhoneInfo />
</div>
)}
<iframe
ref={previewRef}
// Allow all sandbox rules
sandbox={undefined}
src={new URL('/sign-in?preview=true', endpoint).toString()}
tabIndex={-1}
title={t('sign_in_exp.preview.title')}
/>
{disabled ? (
<div className={styles.placeholder}>
<div className={styles.title}>{t('sign_in_exp.custom_ui.bring_your_ui_title')}</div>
<div className={styles.description}>
{t('sign_in_exp.custom_ui.preview_with_bring_your_ui_description')}
</div>
</div>
</div>
) : (
<div className={styles.deviceWrapper}>
<div className={classNames(styles.device, styles[String(mode)])}>
{platform !== PreviewPlatform.DesktopWeb && (
<div className={styles.topBar}>
<div className={styles.time}>{format(Date.now(), 'HH:mm')}</div>
<PhoneInfo />
</div>
)}
<iframe
ref={previewRef}
// Allow all sandbox rules
sandbox={undefined}
src={new URL('/sign-in?preview=true', endpoint).toString()}
tabIndex={-1}
title={t('sign_in_exp.preview.title')}
/>
</div>
</div>
)}
</div>
);
}
Expand Down
17 changes: 0 additions & 17 deletions packages/console/src/consts/user-assets.ts

This file was deleted.

Loading

0 comments on commit cb605e8

Please sign in to comment.