Skip to content

Commit

Permalink
Links (#40)
Browse files Browse the repository at this point in the history
Links!!
  • Loading branch information
chimpdev committed Jul 7, 2024
1 parent 2cc46b4 commit 26f0e44
Show file tree
Hide file tree
Showing 20 changed files with 645 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import useGeneralStore from '@/stores/general';
import { useShallow } from 'zustand/react/shallow';

export default function CreateLinkModal() {
const { name, setName, destinationURL, setDestinationURL } = useGeneralStore(useShallow(state => ({
name: state.createLinkModal.name,
setName: state.createLinkModal.setName,
destinationURL: state.createLinkModal.destinationURL,
setDestinationURL: state.createLinkModal.setDestinationURL
})));

return (
<div className='flex flex-col gap-y-4'>
<div className='flex flex-col'>
<h2 className='text-sm font-semibold text-secondary'>Name</h2>
<p className='text-xs text-tertiary'>The name of the link.</p>

<input
type="text"
placeholder={'example'}
className="w-full px-3 py-2 mt-3 text-sm transition-all outline-none placeholder-placeholder text-secondary bg-secondary hover:bg-background focus-visible:bg-background hover:ring-2 ring-purple-500 rounded-xl"
value={name}
onChange={event => setName(event.target.value)}
/>
</div>

<div className='flex flex-col'>
<h2 className='text-sm font-semibold text-secondary'>Destination URL</h2>
<p className='text-xs text-tertiary'>The URL that the link will redirect to.</p>

<input
type="text"
placeholder={'https://example.com'}
className="w-full px-3 py-2 mt-3 text-sm transition-all outline-none placeholder-placeholder text-secondary bg-secondary hover:bg-background focus-visible:bg-background hover:ring-2 ring-purple-500 rounded-xl"
value={destinationURL}
onChange={event => setDestinationURL(event.target.value)}
/>
</div>
</div>
);
}
235 changes: 235 additions & 0 deletions client/app/(account)/account/components/Content/Tabs/MyLinks/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
'use client';

import ErrorState from '@/app/components/ErrorState';
import useAuthStore from '@/stores/auth';
import useModalsStore from '@/stores/modals';
import Link from 'next/link';
import { BsEmojiAngry } from 'react-icons/bs';
import { FiExternalLink, FiLink, FiTrash2 } from 'react-icons/fi';
import { LuPlus } from 'react-icons/lu';
import { useShallow } from 'zustand/react/shallow';
import deleteLink from '@/lib/request/links/deleteLink';
import { toast } from 'sonner';
import CreateLinkModal from '@/app/(account)/account/components/Content/Tabs/MyLinks/CreateLinkModal';
import useGeneralStore from '@/stores/general';
import createLink from '@/lib/request/links/createLink';
import useAccountStore from '@/stores/account';
import { PiWarningCircleFill } from 'react-icons/pi';

export default function MyLinks() {
const user = useAuthStore(state => state.user);
const fetchData = useAccountStore(state => state.fetchData);
const data = useAccountStore(state => state.data);

const canCreateNewLink = user?.premium?.createdAt ?
data.links?.length < 5 : data.links?.length < 1;

const { openModal, disableButton, enableButton, closeModal } = useModalsStore(useShallow(state => ({
openModal: state.openModal,
disableButton: state.disableButton,
enableButton: state.enableButton,
closeModal: state.closeModal
})));

function continueDeleteLink(id) {
disableButton('delete-link', 'confirm');

toast.promise(deleteLink(id), {
loading: 'Link deleting..',
success: () => {
closeModal('delete-link');
fetchData(['links']);

return 'Successfully deleted the link.';
},
error: error => {
enableButton('delete-link', 'confirm');

return error;
}
});
}

function continueCreateLink() {
openModal('create-link', {
title: 'Create New Link',
description: 'Create a new link to share with your audience.',
content: <CreateLinkModal />,
buttons: [
{
id: 'cancel',
label: 'Cancel',
variant: 'ghost',
actionType: 'close'
},
{
id: 'create',
label: 'Create',
variant: 'solid',
action: () => {
const name = useGeneralStore.getState().createLinkModal.name;
const destinationURL = useGeneralStore.getState().createLinkModal.destinationURL;
const setName = useGeneralStore.getState().createLinkModal.setName;
const setDestinationURL = useGeneralStore.getState().createLinkModal.setDestinationURL;

if (!name) return toast.error('Please enter a name for your link.');

const nameRegex = /^[a-zA-Z0-9-_]+$/;
if (!nameRegex.test(name)) return toast.error('Link name can only contain letters, numbers, hyphens, and underscores.');
if (name.length > 20) return toast.error('Link name must be less than 20 characters.');

if (!destinationURL) return toast.error('Please enter a destination URL for your link.');

try {
const parsedURL = new URL(destinationURL);
if (parsedURL.protocol !== 'https:') return toast.error('Link destination must be a secure URL.');
if (parsedURL.port) return toast.error('Link destination cannot have a port.');

disableButton('create-link', 'create');

toast.promise(createLink({ name, destinationURL }), {
loading: 'Creating link..',
success: () => {
closeModal('create-link');
setName('');
setDestinationURL('');
fetchData(['links']);

return 'Successfully created the link.';
},
error: error => {
enableButton('create-link', 'create');

return error;
}
});
} catch {
return toast.error('Please enter a valid URL.');
}
}
}
]
});
}

return (
<div className='flex flex-col px-6 my-16 lg:px-16 gap-y-6'>
<div className='flex flex-col gap-y-2'>
<h1 className='flex items-center text-xl font-bold gap-x-2 text-primary'>
My Links

<span className='text-xs font-semibold text-tertiary'>
{data.links?.length || 0}/{user?.premium?.createdAt ? 5 : 1}
</span>
</h1>

<p className='text-sm text-secondary'>
View or manage the links that you have created.
</p>
</div>

<div className='max-w-[800px] flex flex-col gap-y-4'>
<div className='flex flex-col gap-y-2'>
{canCreateNewLink ? (
<div
className='flex w-full gap-4 p-4 cursor-pointer rounded-3xl bg-secondary hover:bg-quaternary'
onClick={continueCreateLink}
>
<LuPlus className='text-primary' size={20} />

<span className='text-sm font-medium text-tertiary'>
New Link
</span>
</div>
) : (
<div className='flex flex-col w-full p-4 mt-4 border border-yellow-500 rounded-xl bg-yellow-500/10 gap-y-2'>
<h2 className='flex items-center text-lg font-bold gap-x-2 text-primary'>
<PiWarningCircleFill /> Maximum Links Reached
</h2>
<p className='text-sm font-medium text-tertiary'>
You have reached the maximum amount of links that you can create.<br />
For more information about Premium, visit <Link href='/premium' className='text-secondary hover:text-primary'>Premium page</Link>.
</p>
</div>
)}
</div>

{data.links?.length === 0 ? (
<ErrorState
title={
<div className='flex items-center mt-8 gap-x-2'>
<BsEmojiAngry />
It{'\''}s quiet in here...
</div>
}
message={'You have not created any links.'}
/>
) : (
data.links?.map(link => (
<div
key={link.id}
className='flex items-center gap-4 p-4 bg-secondary rounded-3xl hover:bg-quaternary'
>
<FiLink className='text-primary' size={20} />

<span className='flex items-center w-full text-sm font-medium text-secondary'>
dsc.ink/{link.name}

<span className='ml-2 text-xs font-medium truncate text-tertiary max-w-[70%]'>
({link.redirectTo})
</span>

<div className='flex items-center ml-auto text-xs font-medium gap-x-2 text-tertiary'>
<span className='flex items-center gap-x-1'>
{link.visits} Visits
</span>

<Link
href={link.redirectTo}
target='_blank'
rel='noreferrer'
className='hover:opacity-70'
>
<FiExternalLink size={15} />
</Link>

<button
className='hover:opacity-70'
onClick={() =>
openModal('delete-link', {
title: 'Delete Link',
description: 'Are you sure you want to delete?',
content: (
<p className='text-sm text-tertiary'>
Please note that deleting your link will remove all visits that your link has received.<br/><br/>
This action cannot be undone.
</p>
),
buttons: [
{
id: 'cancel',
label: 'Cancel',
variant: 'ghost',
actionType: 'close'
},
{
id: 'confirm',
label: 'Confirm',
variant: 'solid',
action: () => continueDeleteLink(link.id)
}
]
})
}
>
<FiTrash2 size={15} />
</button>
</div>
</span>
</div>
))
)}
</div>
</div>
);
}
34 changes: 27 additions & 7 deletions client/app/(account)/account/components/Content/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MdAccountCircle, MdDarkMode, MdEmojiEmotions, MdSunny } from 'react-ico
import { MdAccessTimeFilled } from 'react-icons/md';
import ActiveTimeouts from '@/app/(account)/account/components/Content/Tabs/ActiveTimeouts';
import ActiveReminders from '@/app/(account)/account/components/Content/Tabs/ActiveReminders';
import MyLinks from '@/app/(account)/account/components/Content/Tabs/MyLinks';
import MyServers from '@/app/(account)/account/components/Content/Tabs/MyServers';
import MyBots from '@/app/(account)/account/components/Content/Tabs/MyBots';
import MyEmojis from '@/app/(account)/account/components/Content/Tabs/MyEmojis';
Expand All @@ -29,6 +30,7 @@ import { useMedia } from 'react-use';
import { IoChevronBackOutline } from 'react-icons/io5';
import { nanoid } from 'nanoid';
import { PiWaveformBold } from 'react-icons/pi';
import { FiLink } from 'react-icons/fi';

export default function Content() {
const user = useAuthStore(state => state.user);
Expand Down Expand Up @@ -76,6 +78,13 @@ export default function Content() {
{
name: 'Your Public Content',
items: [
{
Icon: FiLink,
name: 'My Links',
id: 'my-links',
component: <MyLinks />,
new_badge: true
},
{
Icon: FaCompass,
name: 'My Servers',
Expand Down Expand Up @@ -149,6 +158,9 @@ export default function Content() {
case 'active-timeouts':
fetchData(['timeouts']);
break;
case 'my-links':
fetchData(['links']);
break;
case 'my-servers':
fetchData(['servers']);
break;
Expand Down Expand Up @@ -248,7 +260,7 @@ export default function Content() {
<div className='flex flex-col gap-y-1.5 items-start'>
{item.items
.filter(({ condition }) => !condition || condition())
.map(({ Icon, IconEnd, name, id, type, action, badge_count }) => (
.map(({ Icon, IconEnd, name, id, type, action, badge_count, new_badge }) => (
type === 'divider' ? (
<div
key={nanoid()}
Expand Down Expand Up @@ -282,12 +294,20 @@ export default function Content() {
{name}
</span>

<span className={cn(
'px-2 py-0.5 ml-auto text-xs transition-opacity font-semibold rounded-full bg-quaternary text-primary',
((badge_count || 0) === 0 || (activeTab === id && loading)) ? 'opacity-0' : 'opacity-100'
)}>
{badge_count}
</span>
{new_badge && (
<div className='px-2.5 py-0.5 ml-auto text-xs font-bold text-white bg-purple-500 rounded-full'>
NEW
</div>
)}

{badge_count > 0 && (
<span className={cn(
'px-2 py-0.5 ml-auto text-xs transition-opacity font-semibold rounded-full bg-quaternary text-primary',
((badge_count || 0) === 0 || (activeTab === id && loading)) ? 'opacity-0' : 'opacity-100'
)}>
{badge_count}
</span>
)}

{IconEnd && <IconEnd className='ml-auto text-tertiary' />}

Expand Down
Loading

0 comments on commit 26f0e44

Please sign in to comment.