-
-
Notifications
You must be signed in to change notification settings - Fork 482
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ui] Very first draft of the link grid
- Loading branch information
1 parent
c3ecb08
commit daebbf0
Showing
14 changed files
with
248 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
"use client"; | ||
|
||
import APIClient from "@/lib/api"; | ||
import { useRouter } from "next/navigation"; | ||
import { useState } from "react"; | ||
|
||
export default function AddLink() { | ||
const router = useRouter(); | ||
const [link, setLink] = useState(""); | ||
|
||
const bookmarkLink = async () => { | ||
const [_resp, error] = await APIClient.bookmarkLink(link); | ||
if (error) { | ||
alert(error.message); | ||
return; | ||
} | ||
router.refresh(); | ||
}; | ||
|
||
return ( | ||
<div className="p-4"> | ||
<input | ||
type="text" | ||
placeholder="Link" | ||
value={link} | ||
onChange={(val) => setLink(val.target.value)} | ||
onKeyUp={async (event) => { | ||
if (event.key == "Enter") { | ||
bookmarkLink(); | ||
setLink(""); | ||
} | ||
}} | ||
className="w-10/12 px-4 py-2 border rounded-md focus:outline-none focus:border-blue-300" | ||
/> | ||
<button className="w-2/12 px-1 py-2" onClick={bookmarkLink}> | ||
Submit | ||
</button> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { ZBookmarkedLink } from "@/lib/types/api/links"; | ||
import Link from "next/link"; | ||
|
||
export default async function LinkCard({ link }: { link: ZBookmarkedLink }) { | ||
return ( | ||
<Link href={link.url} className="border rounded-md hover:border-blue-300"> | ||
<div className="p-4"> | ||
<h2 className="text-lg font-semibold"> | ||
{link.details?.favicon && ( | ||
// eslint-disable-next-line @next/next/no-img-element | ||
<img alt="" width="10" height="10" src={link.details?.favicon} /> | ||
)} | ||
{link.details?.title ?? link.id} | ||
</h2> | ||
<p className="text-gray-600">{link.details?.description ?? link.url}</p> | ||
</div> | ||
</Link> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { getServerSession } from "next-auth"; | ||
import { redirect } from "next/navigation"; | ||
import { authOptions } from "@/lib/auth"; | ||
import { getLinks } from "@/lib/services/links"; | ||
import LinkCard from "./LinkCard"; | ||
|
||
export default async function LinksGrid() { | ||
const session = await getServerSession(authOptions); | ||
if (!session) { | ||
redirect("/"); | ||
} | ||
const links = await getLinks(session.user.id); | ||
|
||
return ( | ||
<div className="container mx-auto mt-8 grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> | ||
{links.map((l) => ( | ||
<LinkCard key={l.id} link={l} /> | ||
))} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import AddLink from "./components/AddLink"; | ||
import LinksGrid from "./components/LinksGrid"; | ||
|
||
export default async function Bookmarks() { | ||
return ( | ||
<> | ||
<AddLink /> | ||
<LinksGrid /> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
"use client"; | ||
|
||
import { ZodTypeAny, z } from "zod"; | ||
import { | ||
ZNewBookmarkedLinkRequest, | ||
zGetLinksResponseSchema, | ||
} from "./types/api/links"; | ||
|
||
import serverConfig from "./config"; | ||
|
||
const BASE_URL = `${serverConfig.api_url}/api/v1`; | ||
|
||
export type FetchError = { | ||
status?: number; | ||
message?: string; | ||
}; | ||
|
||
async function doRequest<Schema extends ZodTypeAny>( | ||
_path: string, | ||
respSchema: Schema, | ||
_opts: RequestInit | undefined, | ||
): Promise<[z.infer<typeof respSchema>, undefined] | [undefined, FetchError]>; | ||
|
||
async function doRequest<_Schema>( | ||
_path: string, | ||
_respSchema: undefined, | ||
_opts: RequestInit | undefined, | ||
): Promise<[undefined, undefined] | [undefined, FetchError]>; | ||
|
||
type InputSchema<T> = T extends ZodTypeAny ? T : undefined; | ||
|
||
async function doRequest<T>( | ||
path: string, | ||
respSchema?: InputSchema<T>, | ||
opts?: RequestInit, | ||
): Promise< | ||
| (InputSchema<T> extends ZodTypeAny | ||
? [z.infer<InputSchema<T>>, undefined] | ||
: [undefined, undefined]) | ||
| [undefined, FetchError] | ||
> { | ||
try { | ||
const res = await fetch(`${BASE_URL}${path}`, opts); | ||
if (!res.ok) { | ||
return [ | ||
undefined, | ||
{ status: res.status, message: await res.text() }, | ||
] as const; | ||
} | ||
if (!respSchema) { | ||
return [undefined, undefined] as const; | ||
} | ||
|
||
let parsed = respSchema.safeParse(await res.json()); | ||
if (!parsed.success) { | ||
return [ | ||
undefined, | ||
{ message: `Failed to parse response: ${parsed.error.toString()}` }, | ||
] as const; | ||
} | ||
|
||
return [parsed.data, undefined] as const; | ||
} catch (error: any) { | ||
return [ | ||
undefined, | ||
{ message: `Failed to execute fetch request: ${error}` }, | ||
] as const; | ||
} | ||
} | ||
|
||
export default class APIClient { | ||
static async getLinks() { | ||
return await doRequest(`/links`, zGetLinksResponseSchema, { | ||
next: { tags: ["links"] }, | ||
}); | ||
} | ||
|
||
static async bookmarkLink(url: string) { | ||
const body: ZNewBookmarkedLinkRequest = { | ||
url, | ||
}; | ||
return await doRequest(`/links`, undefined, { | ||
method: "POST", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { LinkCrawlerQueue } from "@remember/shared/queues"; | ||
import prisma from "@remember/db"; | ||
|
||
export async function bookmarkLink(url: string, userId: string) { | ||
const link = await prisma.bookmarkedLink.create({ | ||
data: { | ||
url, | ||
userId, | ||
}, | ||
}); | ||
|
||
// Enqueue crawling request | ||
await LinkCrawlerQueue.add("crawl", { | ||
linkId: link.id, | ||
url: link.url, | ||
}); | ||
|
||
return link; | ||
} | ||
|
||
export async function getLinks(userId: string) { | ||
return await prisma.bookmarkedLink.findMany({ | ||
where: { | ||
userId, | ||
}, | ||
select: { | ||
id: true, | ||
url: true, | ||
createdAt: true, | ||
details: { | ||
select: { | ||
title: true, | ||
description: true, | ||
imageUrl: true, | ||
favicon: true, | ||
}, | ||
}, | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters