mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
feat: create settings modal
This commit is contained in:
12
inertia/components/common/modal/_modal_body.tsx
Normal file
12
inertia/components/common/modal/_modal_body.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const ModalBody = styled.div({
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ModalBody;
|
||||||
23
inertia/components/common/modal/_modal_container.tsx
Normal file
23
inertia/components/common/modal/_modal_container.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const ModalContainer = styled.div(({ theme }) => ({
|
||||||
|
minWidth: '500px',
|
||||||
|
background: theme.colors.secondary,
|
||||||
|
padding: '1em',
|
||||||
|
borderRadius: theme.border.radius,
|
||||||
|
marginTop: '6em',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
boxShadow: theme.colors.boxShadow,
|
||||||
|
|
||||||
|
[`@media (max-width: ${theme.media.mobile})`]: {
|
||||||
|
maxHeight: 'calc(100% - 2em)',
|
||||||
|
width: 'calc(100% - 2em)',
|
||||||
|
minWidth: 'unset',
|
||||||
|
marginTop: '1em',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default ModalContainer;
|
||||||
20
inertia/components/common/modal/_modal_header.tsx
Normal file
20
inertia/components/common/modal/_modal_header.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const ModalHeader = styled.h3({
|
||||||
|
width: '100%',
|
||||||
|
marginBottom: '0.75em',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalCloseBtn = styled.button(({ theme }) => ({
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: theme.colors.primary,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 0,
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export { ModalHeader, ModalCloseBtn };
|
||||||
18
inertia/components/common/modal/_modal_wrapper.tsx
Normal file
18
inertia/components/common/modal/_modal_wrapper.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { rgba } from '~/lib/color';
|
||||||
|
|
||||||
|
const ModalWrapper = styled.div(({ theme }) => ({
|
||||||
|
zIndex: 9999,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
background: rgba(theme.colors.black, 0.35),
|
||||||
|
backdropFilter: 'blur(0.25em)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default ModalWrapper;
|
||||||
59
inertia/components/common/modal/modal.tsx
Normal file
59
inertia/components/common/modal/modal.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Fragment, ReactNode, useEffect, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { IoClose } from 'react-icons/io5';
|
||||||
|
import ModalBody from '~/components/common/modal/_modal_body';
|
||||||
|
import ModalContainer from '~/components/common/modal/_modal_container';
|
||||||
|
import {
|
||||||
|
ModalCloseBtn,
|
||||||
|
ModalHeader,
|
||||||
|
} from '~/components/common/modal/_modal_header';
|
||||||
|
import ModalWrapper from '~/components/common/modal/_modal_wrapper';
|
||||||
|
import TextEllipsis from '~/components/common/text_ellipsis';
|
||||||
|
import useClickOutside from '~/hooks/use_click_outside';
|
||||||
|
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
|
||||||
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
title?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
opened: boolean;
|
||||||
|
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Modal({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
opened = true,
|
||||||
|
close,
|
||||||
|
}: ModalProps) {
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { setGlobalHotkeysEnabled } = useGlobalHotkeys();
|
||||||
|
|
||||||
|
useClickOutside(modalRef, close);
|
||||||
|
useShortcut('ESCAPE_KEY', close, { ignoreGlobalHotkeysStatus: true });
|
||||||
|
|
||||||
|
useEffect(() => setGlobalHotkeysEnabled(!opened), [opened]);
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
opened &&
|
||||||
|
createPortal(
|
||||||
|
<ModalWrapper>
|
||||||
|
<ModalContainer ref={modalRef}>
|
||||||
|
<ModalHeader>
|
||||||
|
{title && <TextEllipsis>{title}</TextEllipsis>}
|
||||||
|
<ModalCloseBtn onClick={close}>
|
||||||
|
<IoClose size={20} />
|
||||||
|
</ModalCloseBtn>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>{children}</ModalBody>
|
||||||
|
</ModalContainer>
|
||||||
|
</ModalWrapper>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { rgba } from '~/lib/color';
|
||||||
|
|
||||||
const RoundedImage = styled.img({
|
const RoundedImage = styled.img(({ theme }) => {
|
||||||
'borderRadius': '50%',
|
const transparentBlack = rgba(theme.colors.black, 0.1);
|
||||||
|
return {
|
||||||
|
borderRadius: '50%',
|
||||||
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
boxShadow: '0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1)',
|
boxShadow: `0 1px 3px 0 ${transparentBlack}, 0 1px 2px -1px ${transparentBlack}`,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export default RoundedImage;
|
export default RoundedImage;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const LinksWrapper = styled.div({
|
|||||||
const CollectionHeaderWrapper = styled.h2(({ theme }) => ({
|
const CollectionHeaderWrapper = styled.h2(({ theme }) => ({
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
color: theme.colors.font,
|
color: theme.colors.font,
|
||||||
paddingRight: '1.1em',
|
paddingInline: '0.8em 1.1em',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '0.4em',
|
gap: '0.4em',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
|
|
||||||
export const Item = styled.div(({ theme }) => ({
|
export const Item = styled.div(({ theme }) => ({
|
||||||
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
width: '250px',
|
width: '250px',
|
||||||
|
|||||||
25
inertia/components/dashboard/side_nav/open_settings.tsx
Normal file
25
inertia/components/dashboard/side_nav/open_settings.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { BsGear } from 'react-icons/bs';
|
||||||
|
import Modal from '~/components/common/modal/modal';
|
||||||
|
import { Item } from '~/components/dashboard/side_nav/nav_item';
|
||||||
|
import useToggle from '~/hooks/use_modal';
|
||||||
|
|
||||||
|
const SettingsButton = styled(Item)(({ theme }) => ({
|
||||||
|
color: theme.colors.grey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function OpenSettingsButton() {
|
||||||
|
const { isShowing, open, close } = useToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsButton onClick={open}>
|
||||||
|
<BsGear />
|
||||||
|
Settings
|
||||||
|
</SettingsButton>
|
||||||
|
<Modal title="Settings" opened={isShowing} close={close}>
|
||||||
|
Modal settings
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { AiOutlineFolderAdd } from 'react-icons/ai';
|
import { AiOutlineFolderAdd } from 'react-icons/ai';
|
||||||
import { BsGear } from 'react-icons/bs';
|
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { MdOutlineAdminPanelSettings } from 'react-icons/md';
|
import { MdOutlineAdminPanelSettings } from 'react-icons/md';
|
||||||
import FavoriteList from '~/components/dashboard/side_nav/favorite/favorite_list';
|
import FavoriteList from '~/components/dashboard/side_nav/favorite/favorite_list';
|
||||||
import { Item, ItemLink } from '~/components/dashboard/side_nav/nav_item';
|
import { Item, ItemLink } from '~/components/dashboard/side_nav/nav_item';
|
||||||
|
import OpenSettingsButton from '~/components/dashboard/side_nav/open_settings';
|
||||||
import UserCard from '~/components/dashboard/side_nav/user_card';
|
import UserCard from '~/components/dashboard/side_nav/user_card';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
import useActiveCollection from '~/hooks/use_active_collection';
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
@@ -21,10 +21,6 @@ const AdminButton = styled(Item)(({ theme }) => ({
|
|||||||
color: theme.colors.lightRed,
|
color: theme.colors.lightRed,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const SettingsButton = styled(Item)(({ theme }) => ({
|
|
||||||
color: theme.colors.grey,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const AddButton = styled(ItemLink)(({ theme }) => ({
|
const AddButton = styled(ItemLink)(({ theme }) => ({
|
||||||
color: theme.colors.primary,
|
color: theme.colors.primary,
|
||||||
}));
|
}));
|
||||||
@@ -38,10 +34,7 @@ export default function SideNavigation() {
|
|||||||
<AdminButton>
|
<AdminButton>
|
||||||
<MdOutlineAdminPanelSettings /> Administrator
|
<MdOutlineAdminPanelSettings /> Administrator
|
||||||
</AdminButton>
|
</AdminButton>
|
||||||
<SettingsButton>
|
<OpenSettingsButton />
|
||||||
<BsGear />
|
|
||||||
Settings
|
|
||||||
</SettingsButton>
|
|
||||||
<AddButton
|
<AddButton
|
||||||
href={appendCollectionId(
|
href={appendCollectionId(
|
||||||
route('link.create-form').url,
|
route('link.create-form').url,
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import KEYS from '#constants/keys';
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
|
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
|
||||||
|
|
||||||
export default function useShortcut(key: keyof typeof KEYS, cb: () => void) {
|
type ShortcutOptions = { ignoreGlobalHotkeysStatus?: boolean };
|
||||||
|
|
||||||
|
export default function useShortcut(
|
||||||
|
key: keyof typeof KEYS,
|
||||||
|
cb: () => void,
|
||||||
|
options: ShortcutOptions = {
|
||||||
|
ignoreGlobalHotkeysStatus: false,
|
||||||
|
}
|
||||||
|
) {
|
||||||
const { globalHotkeysEnabled } = useGlobalHotkeys();
|
const { globalHotkeysEnabled } = useGlobalHotkeys();
|
||||||
return useHotkeys(KEYS[key], cb, {
|
return useHotkeys(KEYS[key], cb, {
|
||||||
enabled: globalHotkeysEnabled,
|
enabled: !options.ignoreGlobalHotkeysStatus ? globalHotkeysEnabled : true,
|
||||||
enableOnFormTags: ['INPUT'],
|
enableOnFormTags: ['INPUT'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
6
inertia/lib/color.ts
Normal file
6
inertia/lib/color.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import hexRgb from 'hex-rgb';
|
||||||
|
|
||||||
|
export const rgba = (hex: string, alpha: number) => {
|
||||||
|
const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(',');
|
||||||
|
return `rgba(${rgb},${alpha})`;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Theme } from '@emotion/react';
|
import { Theme } from '@emotion/react';
|
||||||
|
import { rgba } from '~/lib/color';
|
||||||
|
|
||||||
export const primaryColor = '#3f88c5';
|
export const primaryColor = '#3f88c5';
|
||||||
export const primaryDarkColor = '#005aa5';
|
export const primaryDarkColor = '#005aa5';
|
||||||
@@ -32,6 +33,7 @@ export const lightTheme: Theme = {
|
|||||||
primary: primaryColor,
|
primary: primaryColor,
|
||||||
secondary: '#fff',
|
secondary: '#fff',
|
||||||
|
|
||||||
|
black: '#333',
|
||||||
white: '#ffffff',
|
white: '#ffffff',
|
||||||
|
|
||||||
lightGrey: '#dadce0',
|
lightGrey: '#dadce0',
|
||||||
@@ -45,7 +47,7 @@ export const lightTheme: Theme = {
|
|||||||
|
|
||||||
yellow: '#FF8A08',
|
yellow: '#FF8A08',
|
||||||
|
|
||||||
boxShadow: '0 0 1em 0 rgba(102, 102, 102, 0.25)',
|
boxShadow: `0 0 1em 0 ${rgba('#aaa', 0.4)}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
border,
|
border,
|
||||||
@@ -60,6 +62,7 @@ export const darkTheme: Theme = {
|
|||||||
primary: '#4fadfc',
|
primary: '#4fadfc',
|
||||||
secondary: '#323a47',
|
secondary: '#323a47',
|
||||||
|
|
||||||
|
black: '#333',
|
||||||
white: '#ffffff',
|
white: '#ffffff',
|
||||||
|
|
||||||
lightGrey: '#323a47',
|
lightGrey: '#323a47',
|
||||||
@@ -73,7 +76,7 @@ export const darkTheme: Theme = {
|
|||||||
|
|
||||||
yellow: '#ffc107',
|
yellow: '#ffc107',
|
||||||
|
|
||||||
boxShadow: '0 0 1em 0 rgb(40 40 40)',
|
boxShadow: `0 0 1em 0 ${rgba('#111', 0.4)}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
border,
|
border,
|
||||||
|
|||||||
1
inertia/types/emotion.d.ts
vendored
1
inertia/types/emotion.d.ts
vendored
@@ -8,6 +8,7 @@ declare module '@emotion/react' {
|
|||||||
primary: string;
|
primary: string;
|
||||||
secondary: string;
|
secondary: string;
|
||||||
|
|
||||||
|
black: string;
|
||||||
white: string;
|
white: string;
|
||||||
|
|
||||||
lightGrey: string;
|
lightGrey: string;
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"@izzyjs/route": "^1.1.0-0",
|
"@izzyjs/route": "^1.1.0-0",
|
||||||
"@vinejs/vine": "^2.0.0",
|
"@vinejs/vine": "^2.0.0",
|
||||||
"edge.js": "^6.0.2",
|
"edge.js": "^6.0.2",
|
||||||
|
"hex-rgb": "^5.0.0",
|
||||||
"i18next": "^23.11.3",
|
"i18next": "^23.11.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"node-html-parser": "^6.1.13",
|
"node-html-parser": "^6.1.13",
|
||||||
@@ -6399,6 +6400,17 @@
|
|||||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/hex-rgb": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hoist-non-react-statics": {
|
"node_modules/hoist-non-react-statics": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"@izzyjs/route": "^1.1.0-0",
|
"@izzyjs/route": "^1.1.0-0",
|
||||||
"@vinejs/vine": "^2.0.0",
|
"@vinejs/vine": "^2.0.0",
|
||||||
"edge.js": "^6.0.2",
|
"edge.js": "^6.0.2",
|
||||||
|
"hex-rgb": "^5.0.0",
|
||||||
"i18next": "^23.11.3",
|
"i18next": "^23.11.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"node-html-parser": "^6.1.13",
|
"node-html-parser": "^6.1.13",
|
||||||
|
|||||||
Reference in New Issue
Block a user