Skip to content

Update production from development #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"next-auth": "^4.24.7",
"next-nprogress-bar": "^2.3.11",
"nextjs-toploader": "^1.6.12",
"qr-code-styling": "^1.9.1",
"react": "^18.3.1",
"react-data-table-component": "^7.6.2",
"react-dom": "^18.3.1",
Expand Down
118 changes: 72 additions & 46 deletions src/app/(admin)/admin/link/_components/LinkFigure.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import { useEffect, useState } from "react";
import { FaGlobeAsia } from "react-icons/fa";
import { FaGlobeAsia, FaQrcode } from "react-icons/fa";
import { toast } from "sonner";
import ClipboardJS from "clipboard";

import { deleteLink } from "@/actions/link";
import { H3, P } from "@/app/_components/global/Text";
Expand All @@ -18,12 +19,14 @@ import {
UserIcon,
} from "./Icons";
import Modal from "./Modal";
import ClipboardJS from "clipboard";
import QRModal from "./QRModal";

export default function LinkFigure({
link,
}: Readonly<{ link: LinkWithCountAndUser }>) {
const [isOpenModal, setIsOpenModal] = useState(false);
const [isOpenQRModal, setIsOpenQRModal] = useState(false);
const fullUrl = `https://go.moklet.org/${link.slug}`;

useEffect(() => {
const clipboard = new ClipboardJS(".copy");
Expand All @@ -50,72 +53,95 @@ export default function LinkFigure({
if (!result.error) toast.success(result.message, { id: toastId });
else toast.error(result.message, { id: toastId });
}

return (
<figure className="lg:flex justify-between w-full bg-white rounded-xl px-6 py-4">
<figure className="w-full bg-white rounded-xl p-4 md:p-6">
{isOpenModal && <Modal setIsOpenModal={setIsOpenModal} link={link} />}
<div className="flex flex-col gap-2">
<div className="flex gap-4 items-start">
<span className="p-2 lg:inline-block hidden rounded-full border">
<FaGlobeAsia className="text-3xl text-gray-400" />
{isOpenQRModal && (
<QRModal setIsOpenQRModal={setIsOpenQRModal} url={fullUrl} />
)}

<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div className="flex flex-col md:flex-row gap-2 md:gap-4 md:items-start">
<span className="p-2 hidden md:inline-flex rounded-full border">
<FaGlobeAsia className="text-2xl md:text-3xl text-gray-400" />
</span>
<div className="text-wrap">
<H3 className="lg:text-[28px] text-[20px]">
<div className="text-wrap max-w-full overflow-hidden">
<H3 className="text-xl md:text-2xl lg:text-[28px]">
<span
data-clipboard-text={"https://go.moklet.org/" + link.slug}
data-clipboard-text={fullUrl}
className="copy text-black text-wrap break-all hover:text-gray-8 font-semibold hover:cursor-pointer transition-all duration-500 cursor-pointer"
>
{"go.moklet.org/" + link.slug}
<span className="inline-block">go.moklet.org/</span>
<span className="inline-block truncate max-w-[150px] sm:max-w-[200px] md:max-w-[250px] lg:max-w-[350px] align-bottom">
{link.slug}
</span>
</span>
</H3>
<P className="max-w-full break-all">{link.target_url}</P>
<div className="pt-5 flex gap-4 flex-col lg:flex-row">
<span className="flex gap-1 items-center ">
<P className="line-clamp-1 xl:line-clamp-2 break-all w-full md:max-w-lg">
{link.target_url}
</P>
<div className="pt-3 md:pt-5 flex flex-wrap gap-3 md:gap-4">
<span className="flex gap-1 items-center">
<StatsIcon />

<P>{link.count?.click_count}</P>
</span>
<span className="flex gap-1 items-center">
<DateIcon />

<P>{stringifyDate(link.created_at)}</P>
</span>
<span className="flex gap-1 items-center">
<UserIcon />

<P>{link.user.name}</P>
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2 md:pt-1 pt-5">
<button
data-clipboard-text={"https://go.moklet.org/" + link.slug}
className="copy group border border-primary-400 px-6 py-3 rounded-xl hover:bg-primary-400/50 transition-all duration-500"
>
<span className="flex items-center gap-2 transition-all duration-500">
<CopyIcon />
<P className="text-lg font-semibold text-primary-400 transition-all duration-500">
Copy
</P>
</span>
</button>
<button
onClick={() => setIsOpenModal(true)}
className="group border border-gray-400 px-3 py-3 rounded-xl hover:bg-gray-100 transition-all duration-500"
>
<span className="flex items-center gap-2 transition-all duration-500">
<EditIcon />
</span>
</button>
<button
onClick={() => deleteAction(link.slug)}
className="group border border-gray-400 px-3 py-3 rounded-xl hover:bg-gray-100 transition-all duration-500"
>
<span className="flex items-center gap-2 transition-all duration-500">
<DeleteIcon />
</span>
</button>

<div className="flex flex-wrap items-center gap-2 mt-4 md:mt-0">
<button
data-clipboard-text={fullUrl}
title="Copy Link"
className="copy group border border-primary-400 px-4 sm:px-6 py-2 sm:py-3 rounded-xl hover:bg-primary-400/50 transition-all duration-500"
>
<span className="flex items-center gap-2 transition-all duration-500">
<CopyIcon />
<P className="text-base md:text-lg font-semibold text-primary-400 transition-all duration-500">
Copy
</P>
</span>
</button>

<button
onClick={() => setIsOpenQRModal(true)}
title="Generate QR"
className="group border border-primary-400 px-3 py-2 sm:py-3 rounded-xl hover:bg-primary-400/50 transition-all duration-500"
>
<span className="text-primary-400 flex items-center gap-2 transition-all duration-500">
<FaQrcode size={24} className="md:w-6 md:h-6" />
</span>
</button>

<button
onClick={() => setIsOpenModal(true)}
title="Edit Link"
className="group border border-gray-400 px-3 py-2 sm:py-3 rounded-xl hover:bg-gray-100 transition-all duration-500"
>
<span className="flex items-center gap-2 transition-all duration-500">
<EditIcon />
</span>
</button>

<button
onClick={() => deleteAction(link.slug)}
title="Delete Link"
className="group border border-gray-400 px-3 py-2 sm:py-3 rounded-xl hover:bg-gray-100 transition-all duration-500"
>
<span className="flex items-center gap-2 transition-all duration-500">
<DeleteIcon />
</span>
</button>
</div>
</div>
</figure>
);
Expand Down
41 changes: 28 additions & 13 deletions src/app/(admin)/admin/link/_components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import FormButton from "./part/SubmitButton";
export default function Modal({
setIsOpenModal,
link,
}: {
}: Readonly<{
setIsOpenModal: Dispatch<SetStateAction<boolean>>;
link?: LinkWithCountAndUser;
}) {
}>) {
const [password, setPassword] = useState(!!link?.password);

async function update(formdata: FormData) {
const toastId = toast.loading("Loading...");
const result = await updateLink(formdata);
Expand All @@ -25,59 +26,73 @@ export default function Modal({
setIsOpenModal(false);
} else toast.error(result.message, { id: toastId });
}

return (
<div className="bg-gray-300/50 fixed w-[calc(100%-20rem)] z-10 justify-center items-center top-0 right-0 h-full m-auto">
<div className="relative p-4 w-full h-full max-w-2xl max-h-full m-auto top-20">
<div className="relative bg-white rounded-lg">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/50 p-4 sm:p-6 md:p-8">
<div className="w-full max-w-md md:max-w-lg lg:max-w-2xl mx-auto">
<div className="relative bg-white rounded-lg shadow-xl overflow-hidden">
<form action={update}>
<div className="flex items-center justify-between p-4 md:p-5 border-b">
<H3>Link Shortener</H3>
<div className="flex items-center justify-between p-3 sm:p-4 md:p-5 border-b">
<H3 className="text-lg sm:text-xl md:text-2xl">Link Shortener</H3>
<button
type="button"
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center transition-all"
onClick={() => setIsOpenModal(false)}
>
<FaX size={25} />
<FaX size={20} className="sm:size-22 md:size-25" />
<span className="sr-only">Tutup</span>
</button>
</div>
<div className="p-4 md:p-5 space-y-4">

<div className="p-3 sm:p-4 md:p-5 space-y-3 sm:space-y-4">
<TextField
type="text"
name="target_url"
required
value={link?.target_url}
label="Target Link"
placeholder="https://example.com/blablablablablabla"
className="w-full"
/>

<TextField
type="text"
name="slug"
label="Short URL"
value={link?.slug}
placeholder="mylink"
required
className="w-full"
/>

{password && (
<TextField
type="password"
label="Password"
placeholder="*******"
name="password"
className="w-full"
/>
)}
<span className="flex gap-1">

<span className="flex items-center gap-2">
<input
id="password"
type="checkbox"
name="private_url"
defaultChecked={!!link?.password}
className="p-2 text-primary-500 accent-primary-500 transition-all"
className="w-4 h-4 text-primary-500 accent-primary-500 transition-all"
onChange={() => setPassword(!password)}
/>
<label htmlFor="password">URL Pribadi</label>
<label htmlFor="password" className="text-sm sm:text-base">
URL Pribadi
</label>
</span>

<input type="hidden" name="id" value={link?.slug} />
</div>
<div className="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b justify-end">

<div className="flex items-center justify-end p-3 sm:p-4 md:p-5 border-t border-gray-200">
<FormButton />
</div>
</form>
Expand Down
39 changes: 26 additions & 13 deletions src/app/(admin)/admin/link/_components/ModalCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import FormButton from "./part/SubmitButton";

export default function ModalCreate({
setIsOpenModal,
}: {
}: Readonly<{
setIsOpenModal: Dispatch<SetStateAction<boolean>>;
link?: LinkWithCountAndUser;
}) {
}>) {
const ref = useRef<HTMLFormElement>(null);
const [password, setPassword] = useState(false);

async function create(formdata: FormData) {
const toastId = toast.loading("Loading...");
const result = await addLink(formdata);
Expand All @@ -30,52 +31,64 @@ export default function ModalCreate({
}

return (
<div className="bg-gray-300/50 fixed w-full lg:w-[calc(100%-20rem)] z-10 justify-center items-center top-0 right-0 h-[calc(100%-1rem)] m-auto">
<div className="relative p-4 w-full h-full max-w-2xl max-h-full m-auto top-20">
<div className="relative bg-white rounded-lg">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/50 p-4 sm:p-6 md:p-8">
<div className="w-full max-w-md md:max-w-lg lg:max-w-2xl mx-auto">
<div className="relative bg-white rounded-lg shadow-xl overflow-hidden">
<form ref={ref} action={create}>
<div className="flex items-center justify-between p-4 md:p-5 border-b">
<H3>Create Link</H3>
<div className="flex items-center justify-between p-3 sm:p-4 md:p-5 border-b">
<H3 className="text-lg sm:text-xl md:text-2xl">Create Link</H3>
<button
type="button"
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center transition-all"
onClick={() => setIsOpenModal(false)}
>
<FaX size={25} />
<FaX size={20} className="sm:size-22 md:size-25" />
<span className="sr-only">Tutup</span>
</button>
</div>
<div className="p-4 md:p-5 space-y-4">

<div className="p-3 sm:p-4 md:p-5 space-y-3 sm:space-y-4">
<TextField
type="url"
label="Destination"
name="destLink"
placeholder="https://example.com/thisisaverylongstringthatyouwouldliketoshorten"
required={true}
className="w-full"
/>

<TextField
type="text"
label="Short URL"
name="slug"
placeholder="MokletHebat"
className="w-full"
/>

{password && (
<TextField
type="password"
label="Password"
placeholder="*******"
name="password"
className="w-full"
/>
)}
<span className="flex gap-1">

<span className="flex items-center gap-2">
<input
id="password"
type="checkbox"
className="p-2 text-primary-500 accent-primary-500 transition-all"
className="w-4 h-4 text-primary-500 accent-primary-500 transition-all"
onChange={() => setPassword(!password)}
/>
<label htmlFor="password">URL Pribadi</label>
<label htmlFor="password" className="text-sm sm:text-base">
URL Pribadi
</label>
</span>
</div>
<div className="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b justify-end">

<div className="flex items-center justify-end p-3 sm:p-4 md:p-5 border-t border-gray-200">
<FormButton />
</div>
</form>
Expand Down
Loading