mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
added color picker and avatar upload to profile
This commit is contained in:
parent
771c8f06bc
commit
142a4f86ee
10
package-lock.json
generated
10
package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
"@types/offscreencanvas": "^2019.7.1",
|
"@types/offscreencanvas": "^2019.7.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-image-crop": "^10.1.8",
|
"react-image-crop": "^10.1.8",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-leaflet-cluster": "^2.1.0",
|
"react-leaflet-cluster": "^2.1.0",
|
||||||
@ -3787,6 +3788,15 @@
|
|||||||
"resolved": "../react18-app/node_modules/react",
|
"resolved": "../react18-app/node_modules/react",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-colorful": {
|
||||||
|
"version": "5.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||||
|
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
"@types/offscreencanvas": "^2019.7.1",
|
"@types/offscreencanvas": "^2019.7.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-image-crop": "^10.1.8",
|
"react-image-crop": "^10.1.8",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-leaflet-cluster": "^2.1.0",
|
"react-leaflet-cluster": "^2.1.0",
|
||||||
|
|||||||
20
src/Components/Profile/ColorPicker.css
Normal file
20
src/Components/Profile/ColorPicker.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.picker {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 36px;
|
||||||
|
border-radius: 9px;
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
29
src/Components/Profile/ColorPicker.tsx
Normal file
29
src/Components/Profile/ColorPicker.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { HexColorPicker } from "react-colorful";
|
||||||
|
import "./ColorPicker.css"
|
||||||
|
import useClickOutside from "./useClickOutside";
|
||||||
|
|
||||||
|
export const ColorPicker = ({ color, onChange, className }) => {
|
||||||
|
const popover = useRef<HTMLDivElement>(null);
|
||||||
|
const [isOpen, toggle] = useState(false);
|
||||||
|
|
||||||
|
const close = useCallback(() => toggle(false), []);
|
||||||
|
useClickOutside(popover, close);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`picker ${className}`}>
|
||||||
|
<div
|
||||||
|
className="swatch"
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
onClick={() => toggle(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="popover" ref={popover}>
|
||||||
|
<HexColorPicker color={color} onChange={onChange} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -12,6 +12,7 @@ import 'react-toastify/dist/ReactToastify.css';
|
|||||||
import { UserItem } from '../../types';
|
import { UserItem } from '../../types';
|
||||||
import DialogModal from '../Templates/DialogModal';
|
import DialogModal from '../Templates/DialogModal';
|
||||||
import { useAssetApi } from '../AppShell/hooks/useAssets';
|
import { useAssetApi } from '../AppShell/hooks/useAssets';
|
||||||
|
import { ColorPicker } from './ColorPicker';
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const { user, updateUser, loading, token } = useAuth();
|
const { user, updateUser, loading, token } = useAuth();
|
||||||
@ -22,6 +23,8 @@ export function Settings() {
|
|||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
const [password, setPassword] = useState<string>("");
|
const [password, setPassword] = useState<string>("");
|
||||||
const [avatar, setAvatar] = useState<string>("");
|
const [avatar, setAvatar] = useState<string>("");
|
||||||
|
const [color, setColor] = useState<string>("");
|
||||||
|
|
||||||
|
|
||||||
const [passwordChanged, setPasswordChanged] = useState<boolean>(false);
|
const [passwordChanged, setPasswordChanged] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -39,7 +42,8 @@ export function Settings() {
|
|||||||
setText(user?.description ? user.description : "");
|
setText(user?.description ? user.description : "");
|
||||||
setEmail(user?.email ? user.email : "");
|
setEmail(user?.email ? user.email : "");
|
||||||
setPassword(user?.password ? user.password : "");
|
setPassword(user?.password ? user.password : "");
|
||||||
setAvatar(user?.avatar ? user?.avatar : "")
|
setAvatar(user?.avatar ? user?.avatar : ""),
|
||||||
|
setColor(user?.color? user.color : "#aabbcc")
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
const imgRef = useRef<HTMLImageElement>(null)
|
const imgRef = useRef<HTMLImageElement>(null)
|
||||||
@ -140,7 +144,7 @@ export function Settings() {
|
|||||||
const onUpdateUser = () => {
|
const onUpdateUser = () => {
|
||||||
let changedUser = {} as UserItem;
|
let changedUser = {} as UserItem;
|
||||||
|
|
||||||
changedUser = { id: id, first_name: name, description: text, email: email, ...passwordChanged && { password: password }, ...avatar.length > 10 && { avatar: avatar } };
|
changedUser = { id: id, first_name: name, description: text, email: email, color: color, ...passwordChanged && { password: password }, ...avatar.length > 10 && { avatar: avatar } };
|
||||||
|
|
||||||
|
|
||||||
toast.promise(
|
toast.promise(
|
||||||
@ -169,8 +173,8 @@ export function Settings() {
|
|||||||
<label className="custom-file-upload">
|
<label className="custom-file-upload">
|
||||||
<input type="file" accept="image/*" className="tw-file-input tw-w-full tw-max-w-xs" onChange={onImageChange} />
|
<input type="file" accept="image/*" className="tw-file-input tw-w-full tw-max-w-xs" onChange={onImageChange} />
|
||||||
<div className='button tw-btn tw-btn-lg tw-btn-circle tw-animate-none'>
|
<div className='button tw-btn tw-btn-lg tw-btn-circle tw-animate-none'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" className="tw-w-6 tw-h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="tw-w-6 tw-h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{avatar ?
|
{avatar ?
|
||||||
@ -192,6 +196,7 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
<ColorPicker color={color} onChange={setColor} className={"-tw-left-6 tw-top-14 -tw-mr-6"} />
|
||||||
<TextInput placeholder="Name" defaultValue={user?.first_name ? user.first_name : ""} updateFormValue={(v) => setName(v)} containerStyle='tw-grow tw-ml-6 tw-my-auto ' />
|
<TextInput placeholder="Name" defaultValue={user?.first_name ? user.first_name : ""} updateFormValue={(v) => setName(v)} containerStyle='tw-grow tw-ml-6 tw-my-auto ' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
35
src/Components/Profile/useClickOutside.tsx
Normal file
35
src/Components/Profile/useClickOutside.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// Improved version of https://usehooks.com/useOnClickOutside/
|
||||||
|
const useClickOutside = (ref, handler) => {
|
||||||
|
useEffect(() => {
|
||||||
|
let startedInside = false;
|
||||||
|
let startedWhenMounted = false;
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
// Do nothing if `mousedown` or `touchstart` started inside ref element
|
||||||
|
if (startedInside || !startedWhenMounted) return;
|
||||||
|
// Do nothing if clicking ref's element or descendent elements
|
||||||
|
if (!ref.current || ref.current.contains(event.target)) return;
|
||||||
|
|
||||||
|
handler(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateEventStart = (event) => {
|
||||||
|
startedWhenMounted = ref.current;
|
||||||
|
startedInside = ref.current && ref.current.contains(event.target);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", validateEventStart);
|
||||||
|
document.addEventListener("touchstart", validateEventStart);
|
||||||
|
document.addEventListener("click", listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", validateEventStart);
|
||||||
|
document.removeEventListener("touchstart", validateEventStart);
|
||||||
|
document.removeEventListener("click", listener);
|
||||||
|
};
|
||||||
|
}, [ref, handler]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useClickOutside;
|
||||||
Loading…
x
Reference in New Issue
Block a user