mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-03-13 06:30:01 +01:00
profile settings and image editor - WIP
This commit is contained in:
parent
f67fbd1978
commit
c43bbeaa92
4 changed files with 194 additions and 34 deletions
35
src/app/components/image-editor/ImageEditor.css.ts
Normal file
35
src/app/components/image-editor/ImageEditor.css.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, color, config } from 'folds';
|
||||
|
||||
export const ImageEditor = style([
|
||||
DefaultReset,
|
||||
{
|
||||
height: '100%',
|
||||
},
|
||||
]);
|
||||
|
||||
export const ImageEditorHeader = style([
|
||||
DefaultReset,
|
||||
{
|
||||
paddingLeft: config.space.S200,
|
||||
paddingRight: config.space.S200,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
flexShrink: 0,
|
||||
gap: config.space.S200,
|
||||
},
|
||||
]);
|
||||
|
||||
export const ImageEditorContent = style([
|
||||
DefaultReset,
|
||||
{
|
||||
backgroundColor: color.Background.Container,
|
||||
color: color.Background.OnContainer,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
]);
|
||||
|
||||
export const Image = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
});
|
51
src/app/components/image-editor/ImageEditor.tsx
Normal file
51
src/app/components/image-editor/ImageEditor.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds';
|
||||
import * as css from './ImageEditor.css';
|
||||
|
||||
export type ImageEditorProps = {
|
||||
name: string;
|
||||
url: string;
|
||||
requestClose: () => void;
|
||||
};
|
||||
|
||||
export const ImageEditor = as<'div', ImageEditorProps>(
|
||||
({ className, name, url, requestClose, ...props }, ref) => {
|
||||
const handleApply = () => {
|
||||
//
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={classNames(css.ImageEditor, className)}
|
||||
direction="Column"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<Header className={css.ImageEditorHeader} size="400">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<IconButton size="300" radii="300" onClick={requestClose}>
|
||||
<Icon size="50" src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
<Text size="T300" truncate>
|
||||
Image Editor
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No" alignItems="Center" gap="200">
|
||||
<Chip variant="Primary" radii="300" onClick={handleApply}>
|
||||
<Text size="B300">Save</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
</Header>
|
||||
<Box
|
||||
grow="Yes"
|
||||
className={css.ImageEditorContent}
|
||||
justifyContent="Center"
|
||||
alignItems="Center"
|
||||
>
|
||||
<img className={css.Image} src={url} alt={name} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
1
src/app/components/image-editor/index.ts
Normal file
1
src/app/components/image-editor/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ImageEditor';
|
|
@ -1,17 +1,38 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll, Input, Avatar, Button, Chip } from 'folds';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
IconButton,
|
||||
Icon,
|
||||
Icons,
|
||||
Scroll,
|
||||
Input,
|
||||
Avatar,
|
||||
Button,
|
||||
Chip,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Modal,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { Page, PageContent, PageHeader } from '../../components/page';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { SequenceCardStyle } from './styles.css';
|
||||
import { SettingTile } from '../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useUserProfile } from '../../hooks/useUserProfile';
|
||||
import { UserProfile, useUserProfile } from '../../hooks/useUserProfile';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { UserAvatar } from '../../components/user-avatar';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import { copyToClipboard } from '../../utils/dom';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { useFilePicker } from '../../hooks/useFilePicker';
|
||||
import { useObjectURL } from '../../hooks/useObjectURL';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { ImageEditor } from '../../components/image-editor';
|
||||
import { ModalWide } from '../../styles/Modal.css';
|
||||
|
||||
function MatrixId() {
|
||||
const mx = useMatrixClient();
|
||||
|
@ -39,17 +60,95 @@ function MatrixId() {
|
|||
);
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
type ProfileAvatarProps = {
|
||||
profile: UserProfile;
|
||||
userId: string;
|
||||
};
|
||||
function ProfileAvatar({ profile, userId }: ProfileAvatarProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const userId = mx.getUserId()!;
|
||||
const profile = useUserProfile(userId);
|
||||
|
||||
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const avatarUrl = profile.avatarUrl
|
||||
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const [imageFile, setImageFile] = useState<File>();
|
||||
const imageFileURL = useObjectURL(imageFile);
|
||||
|
||||
const pickFile = useFilePicker(setImageFile, false);
|
||||
|
||||
const handleImageCropperClose = useCallback(() => {
|
||||
setImageFile(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
Avatar
|
||||
</Text>
|
||||
}
|
||||
before={
|
||||
<Avatar size="500" radii="300">
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
<Box gap="200">
|
||||
<Button
|
||||
onClick={() => pickFile('image/*')}
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
outlined
|
||||
radii="300"
|
||||
>
|
||||
<Text size="B300">Upload</Text>
|
||||
</Button>
|
||||
{avatarUrl && (
|
||||
<Button size="300" variant="Critical" fill="None" radii="300">
|
||||
<Text size="B300">Remove</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{imageFileURL && (
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: handleImageCropperClose,
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal className={ModalWide} variant="Surface" size="500">
|
||||
<ImageEditor
|
||||
name={imageFile?.name ?? 'Unnamed'}
|
||||
url={imageFileURL}
|
||||
requestClose={handleImageCropperClose}
|
||||
/>
|
||||
</Modal>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
)}
|
||||
</Box>
|
||||
</SettingTile>
|
||||
);
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
const profile = useUserProfile(userId);
|
||||
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Profile</Text>
|
||||
|
@ -59,33 +158,7 @@ function Profile() {
|
|||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
Avatar
|
||||
</Text>
|
||||
}
|
||||
before={
|
||||
<Avatar size="500" radii="300">
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
<Box gap="200">
|
||||
<Button size="300" variant="Secondary" fill="Soft" outlined radii="300">
|
||||
<Text size="B300">Upload</Text>
|
||||
</Button>
|
||||
{avatarUrl && (
|
||||
<Button size="300" variant="Critical" fill="None" radii="300">
|
||||
<Text size="B300">Remove</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</SettingTile>
|
||||
<ProfileAvatar userId={userId} profile={profile} />
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
|
@ -136,7 +209,7 @@ function ContactInformation() {
|
|||
<SettingTile title="Email Address" description="Email address attached to your account.">
|
||||
<Box>
|
||||
{emailIds?.map((email) => (
|
||||
<Chip as="span" variant="Secondary" radii="Pill">
|
||||
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
|
||||
<Text size="T200">{email.address}</Text>
|
||||
</Chip>
|
||||
))}
|
||||
|
|
Loading…
Reference in a new issue