Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 20 additions & 12 deletions app/dashboard/LinkIdCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@ import { Copy, Check, ExternalLink, Link2 } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";

export function LinkIdCard({ username, qrCode }: { username: string; qrCode?: React.ReactNode }) {
export function LinkIdCard({
username,
qrCode,
}: {
username: string;
qrCode?: React.ReactNode;
}) {
const [copied, setCopied] = useState(false);

function copyProfile() {
navigator.clipboard.writeText(`linkid.qzz.io/${username}`);
setCopied(true);
toast.success("Profile link copied");
setTimeout(() => setCopied(false), 1200);
async function copyProfile() {
try {
await navigator.clipboard.writeText(`linkid.qzz.io/${username}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
setCopied(true);
toast.success("Profile link copied successfully!");
setTimeout(() => setCopied(false), 1200);
} catch (error) {
toast.error("Unable to copy profile link. Please try again.");
}
}

return (
Expand All @@ -21,23 +31,21 @@ export function LinkIdCard({ username, qrCode }: { username: string; qrCode?: Re
<Link2 className="h-4 w-4 sm:h-5 sm:w-5" />
Your LinkID
</CardTitle>
<div className="flex-shrink-0">
{qrCode}
</div>
<div className="flex-shrink-0">{qrCode}</div>
</CardHeader>

<CardContent className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
{/* Link box */}
<div className="flex items-center justify-between gap-2 rounded-md bg-muted px-3 py-2 text-sm font-mono w-full sm:w-auto">
<code className="truncate">
linkid.qzz.io/{username}
</code>
<code className="truncate">linkid.qzz.io/{username}</code>

<Button
size="icon"
variant="ghost"
onClick={copyProfile}
className="shrink-0"
aria-label="Copy profile link"
title="Copy profile link"
>
{copied ? (
<Check className="h-4 w-4 text-green-600" />
Expand Down
257 changes: 139 additions & 118 deletions app/dashboard/LinkItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,144 +3,165 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Copy,
Check,
ExternalLink,
Pencil,
X,
Globe,
Trash,
Eye,
EyeOff,
Copy,
Check,
ExternalLink,
Pencil,
X,
Globe,
Trash,
Eye,
EyeOff,
} from "lucide-react";
import toast from "react-hot-toast";
import { useState } from "react";
import { PLATFORM_ICONS } from "@/lib/platformIcons";
import type { Link as ProfileLink } from "@/app/[username]/types/type";

export function LinkItem({
link,
username,
onUpdate,
onToggleVisibility,
onDelete,
link,
username,
onUpdate,
onToggleVisibility,
onDelete,
}: {
link: ProfileLink;
username: string;
onUpdate: (id: string, url: string) => Promise<void>;
onToggleVisibility: (id: string, isPublic: boolean) => Promise<void>;
onDelete: (id: string) => Promise<void>;
link: ProfileLink;
username: string;
onUpdate: (id: string, url: string) => Promise<void>;
onToggleVisibility: (id: string, isPublic: boolean) => Promise<void>;
onDelete: (id: string) => Promise<void>;
}) {
const Icon = PLATFORM_ICONS[link.platform] ?? Globe;
const [editing, setEditing] = useState(false);
const [url, setUrl] = useState(link.url);
const [copied, setCopied] = useState(false);
const Icon = PLATFORM_ICONS[link.platform] ?? Globe;
const [editing, setEditing] = useState(false);
const [url, setUrl] = useState(link.url);
const [copied, setCopied] = useState(false);

function copy() {
navigator.clipboard.writeText(
`linkid.qzz.io/${username}/${link.platform}`
);
setCopied(true);
toast.success("Copied");
setTimeout(() => setCopied(false), 1200);
async function copy() {
try {
await navigator.clipboard.writeText(
`linkid.qzz.io/${username}/${link.platform}`,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
setCopied(true);
toast.success("Link copied successfully!");
setTimeout(() => setCopied(false), 1200);
} catch (error) {
toast.error("Unable to copy link. Please try again.");
}
}

async function save() {
await onUpdate(link.id, url);
setEditing(false);
}
async function save() {
await onUpdate(link.id, url);
setEditing(false);
}

return (
<div className="rounded-md border p-4 space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex gap-3 items-center min-w-0">
<div className="h-10 w-10 bg-muted rounded-md flex items-center justify-center shrink-0">
<Icon className="h-5 w-5" />
</div>
return (
<div className="rounded-md border p-4 space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex gap-3 items-center min-w-0">
<div className="h-10 w-10 bg-muted rounded-md flex items-center justify-center shrink-0">
<Icon className="h-5 w-5" />
</div>

<div className="min-w-0">
<p className="font-medium capitalize">
{link.platform}
</p>
<p className="text-sm text-muted-foreground truncate">
{link.url}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{link.clicks} {link.clicks === 1 ? "click" : "clicks"}
</p>
<p className="mt-1 inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs text-muted-foreground">
{link.isPublic ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
{link.isPublic ? "Public" : "Private"}
</p>
</div>
</div>
<div className="min-w-0">
<p className="font-medium capitalize">{link.platform}</p>
<p className="text-sm text-muted-foreground truncate">{link.url}</p>
<p className="text-xs text-muted-foreground mt-0.5">
{link.clicks} {link.clicks === 1 ? "click" : "clicks"}
</p>
<p className="mt-1 inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs text-muted-foreground">
{link.isPublic ? (
<Eye className="h-3 w-3" />
) : (
<EyeOff className="h-3 w-3" />
)}
{link.isPublic ? "Public" : "Private"}
</p>
</div>
</div>

<div className="flex flex-wrap gap-1 justify-end">
<Button
size="icon"
variant="ghost"
onClick={() => onToggleVisibility(link.id, !link.isPublic)}
aria-label={link.isPublic ? "Make link private" : "Make link public"}
title={link.isPublic ? "Make link private" : "Make link public"}
>
{link.isPublic ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
<div className="flex flex-wrap gap-1 justify-end">
<Button
size="icon"
variant="ghost"
onClick={() => onToggleVisibility(link.id, !link.isPublic)}
aria-label={
link.isPublic ? "Make link private" : "Make link public"
}
title={link.isPublic ? "Make link private" : "Make link public"}
>
{link.isPublic ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>

<Button size="icon" variant="ghost" onClick={copy}>
{copied ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
<Button
size="icon"
variant="ghost"
onClick={copy}
aria-label="Copy link"
title="Copy link"
>
{copied ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>

<a href={link.url} target="_blank" rel="noopener noreferrer" aria-label={`Open ${link.label ?? link.platform} in new tab`}>
<Button size="icon" variant="ghost" title={`Open ${link.label ?? link.platform}`}>
<ExternalLink className="h-4 w-4" />
</Button>
</a>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
aria-label={`Open ${link.label ?? link.platform} in new tab`}
>
<Button
size="icon"
variant="ghost"
title={`Open ${link.label ?? link.platform}`}
>
<ExternalLink className="h-4 w-4" />
</Button>
</a>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

<Button
size="icon"
variant="ghost"
onClick={() => setEditing((v) => !v)}
>
{editing ? (
<X className="h-4 w-4" />
) : (
<Pencil className="h-4 w-4" />
)}
</Button>
</div>
</div>
<Button
size="icon"
variant="ghost"
onClick={() => setEditing((v) => !v)}
>
{editing ? (
<X className="h-4 w-4" />
) : (
<Pencil className="h-4 w-4" />
)}
</Button>
</div>
</div>

{editing && (
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
className="flex-1 px-2 py-4 text-sm"
/>
{editing && (
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
className="flex-1 px-2 py-4 text-sm"
/>

<div className="flex gap-2 justify-end">
<Button size="icon" onClick={save}>
<Check className="h-4 w-4" />
</Button>
<div className="flex gap-2 justify-end">
<Button size="icon" onClick={save}>
<Check className="h-4 w-4" />
</Button>

<Button
size="icon"
variant="destructive"
onClick={() => onDelete(link.id)}
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
)}
<Button
size="icon"
variant="destructive"
onClick={() => onDelete(link.id)}
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
);
)}
</div>
);
}
14 changes: 7 additions & 7 deletions package-lock.json

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