Skip to content

Commit 213d0ca

Browse files
committed
Link tags
1 parent ed968a1 commit 213d0ca

11 files changed

+231
-87
lines changed

app/components/ItemLinks.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Link as RemixLink } from "@remix-run/react";
2+
import { AdminIcon } from "./AdminIcon";
3+
4+
type ItemType = "pattern" | "link";
5+
6+
type ItemTypeInfo = {
7+
baseUrl: string;
8+
column_prefix: string;
9+
};
10+
11+
const itemTypeMap: Record<ItemType, ItemTypeInfo> =
12+
{
13+
"pattern": {
14+
baseUrl: "/patterns/",
15+
column_prefix: "rxp_",
16+
},
17+
"link": {
18+
baseUrl: "/links/",
19+
column_prefix: "rxl_",
20+
},
21+
};
22+
23+
type ItemLinksProps = {
24+
type: ItemType;
25+
id: string;
26+
adminOnly?: boolean;
27+
}
28+
29+
export function ItemLinks({ id, type, adminOnly }: ItemLinksProps) {
30+
const typeInfo = itemTypeMap[type];
31+
return (
32+
<>
33+
<RemixLink to={`${typeInfo.baseUrl}edit.html?${typeInfo.column_prefix}id=${id}`} className="btn btn-sm btn-secondary mx-1">{ adminOnly ? <AdminIcon /> : null } Edit</RemixLink>
34+
<RemixLink to={`${typeInfo.baseUrl}delete.html?${typeInfo.column_prefix}id=${id}`} className="btn btn-sm btn-secondary mx-1">{adminOnly ? <AdminIcon /> : null} Delete</RemixLink>
35+
</>
36+
);
37+
}

app/components/TagTree.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { SerializeFrom } from "@remix-run/node";
2+
import { Link as RemixLink } from "@remix-run/react";
3+
4+
import { Tag } from "./Tag";
5+
6+
7+
export type TagTreeEntry = {
8+
title: string;
9+
url: string;
10+
id?: string;
11+
}
12+
13+
14+
function TagTreeRow(tag: string, currentTag: string, entries: SerializeFrom<TagTreeEntry>[]) {
15+
return (
16+
<details className="mt-2" open={tag === currentTag}>
17+
<summary><Tag tag={tag} url={`?tag=${tag}`} /></summary>
18+
<ul className="mt-1">
19+
{entries?.map((entry) => (
20+
<li key={entry.url}>
21+
<RemixLink to={entry.url}>{entry.title}</RemixLink>
22+
</li>
23+
))}
24+
</ul>
25+
</details>
26+
)
27+
}
28+
29+
export function TagTree(currentTag: string, tagMap: SerializeFrom<{ [key: string]: SerializeFrom<TagTreeEntry>[] }>) {
30+
return (
31+
<>
32+
{Object.entries(tagMap).map(([key, entries]) => TagTreeRow(key, currentTag, entries))}
33+
</>
34+
);
35+
}

app/db/connection.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ function connect() {
1818
}
1919
});
2020
}
21+
const dbconnection = connect();
2122

23+
const dborm = drizzle(dbconnection, { schema });
2224

23-
const db = drizzle(connect(), { schema });
24-
25-
export { connect, db };
25+
export { dbconnection, dborm };

app/routes/links._index.tsx

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { cookieStorage } from "~/services/session.server";
44
import { User } from "~/types/User";
55
import { AlertWidget } from "~/components/AlertWidget";
66
import type { AlertMessage } from "~/types/AlertMessage";
7-
import { db } from "~/db/connection.server";
7+
import { dborm } from "~/db/connection.server";
88
import { regex_link } from "~/db/schema";
99
import { LinkTagUrlBuilder } from "~/util/LinkTagUrlBuilder";
1010
import { TagList } from "~/components/TagList";
1111
import { authenticator } from "~/services/auth.server";
12-
import { PiLockKey } from "react-icons/pi";
1312
import { AdminIcon } from "~/components/AdminIcon";
13+
import { getLinkDomain } from "~/util/getLinkDomain";
14+
import { ItemLinks } from "~/components/ItemLinks";
1415

1516
export const meta: MetaFunction = () => {
1617
return [
@@ -27,10 +28,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
2728
const message = session.get("message");
2829
console.log("loader message", JSON.stringify(message));
2930

30-
const links = await db.select().from(regex_link);
31-
31+
const links = await dborm.select().from(regex_link);
32+
3233
const user = authenticator.isAuthenticated(request);
33-
34+
3435

3536
// Commit the session and return the message
3637
return json(
@@ -51,44 +52,42 @@ export default function Index() {
5152
const message = data.message as AlertMessage | undefined;
5253

5354
const links = data.links;
54-
55+
5556
return (
5657
<>
5758
<div className="d-flex justify-content-between align-items-center">
5859
<h1 className="py-2">Links</h1>
59-
{ user && user.isAdmin ?
60-
<div>
61-
<RemixLink to="/links/add.html" className="btn btn-primary mx-1"><AdminIcon /> Add</RemixLink>
60+
{user && user.isAdmin ?
61+
<div>
62+
<RemixLink to="/links/add.html" className="btn btn-primary mx-1"><AdminIcon /> Add</RemixLink>
6263
<RemixLink to="/links/import.html" className="btn btn-primary mx-1"><AdminIcon /> Import</RemixLink>
63-
</div>
64-
: null }
64+
</div>
65+
: null}
6566
</div>
66-
{message ? <AlertWidget alert={message}/> : null}
67-
{links.length == 0 ? <div className="alert alert-warning">No links found</div> :
68-
<table className="table table-striped table-hover">
69-
<thead className="d-none">
70-
<tr>
71-
<th>Description</th>
72-
<th>Tags</th>
73-
</tr>
74-
</thead>
75-
<tbody>
76-
{links.map(link => (
77-
<tr key={link.rxl_id}>
78-
<td><a href={link.rxl_url}>{link.rxl_title}</a></td>
79-
<td className="text-end">
80-
<TagList tags={link.rxl_tags} urlBuilder={LinkTagUrlBuilder} />
81-
{ user && user.isAdmin ?
82-
<>
83-
<RemixLink to={`/links/edit.html?rxl_id=${link.rxl_id}`} className="btn btn-sm btn-secondary mx-1"><AdminIcon /> Edit</RemixLink>
84-
<RemixLink to={`/links/delete.html?rxl_id=${link.rxl_id}`} className="btn btn-sm btn-secondary mx-1"><AdminIcon /> Delete</RemixLink>
85-
</>
86-
: null }
87-
</td>
67+
{message ? <AlertWidget alert={message} /> : null}
68+
{links.length == 0 ? <div className="alert alert-warning">No links found</div> :
69+
<table className="table table-striped table-hover">
70+
<thead className="d-none">
71+
<tr>
72+
<th>Description</th>
73+
<th>Tags</th>
8874
</tr>
89-
))}
90-
</tbody>
91-
</table>
75+
</thead>
76+
<tbody>
77+
{links.map(link => (
78+
<tr key={link.rxl_id}>
79+
<td>
80+
<a className="me-2" href={link.rxl_url}>{link.rxl_title}</a>
81+
({getLinkDomain(link.rxl_url)})
82+
</td>
83+
<td className="text-end">
84+
<TagList tags={link.rxl_tags.sort()} urlBuilder={LinkTagUrlBuilder} />
85+
{user && user.isAdmin ? <ItemLinks adminOnly={true} type="link" id={link.rxl_id} /> : null}
86+
</td>
87+
</tr>
88+
))}
89+
</tbody>
90+
</table>
9291
}
9392
</>
9493
);

app/routes/links.add[.]html.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
2-
import { json, Link as RemixLink, redirect, useLoaderData } from "@remix-run/react";
3-
import { eq } from "drizzle-orm"
2+
import { json, Link as RemixLink, redirect } from "@remix-run/react";
43

5-
import { db } from "~/db/connection.server";
4+
import { dborm } from "~/db/connection.server";
65
import { regex_link } from "~/db/schema";
76
import { authenticator } from "~/services/auth.server";
87
import { getFormString } from "~/util/getFormString";
@@ -30,7 +29,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
3029
export async function action({ request }: ActionFunctionArgs) {
3130
const formData = await request.formData();
3231

33-
await db.insert(regex_link).values({
32+
await dborm.insert(regex_link).values({
3433
rxl_url: getFormString(formData.get("rxl_url")),
3534
rxl_title: getFormString(formData.get("rxl_title")),
3635
rxl_tags: getFormString(formData.get("rxl_tags")).split(' '),

app/routes/links.delete[.]html.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remi
22
import { json, Link as RemixLink, redirect, useLoaderData } from "@remix-run/react";
33
import { eq } from "drizzle-orm"
44

5-
import { db } from "~/db/connection.server";
5+
import { dborm } from "~/db/connection.server";
66
import { regex_link } from "~/db/schema";
77
import { authenticator } from "~/services/auth.server";
88
import { getFormString } from "~/util/getFormString";
@@ -22,7 +22,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
2222
return redirect("/links/");
2323
}
2424

25-
const links = await db.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
25+
const links = await dborm.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
2626
if (!links || links.length != 1) {
2727
//LATER: flash error
2828
return redirect("/links/");
@@ -48,7 +48,7 @@ export async function action({ request }: ActionFunctionArgs) {
4848
return redirect("/links/");
4949
}
5050

51-
const links = await db.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
51+
const links = await dborm.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
5252
if (!links || links.length != 1) {
5353
//LATER: flash error
5454
return redirect("/links/");
@@ -61,7 +61,7 @@ export async function action({ request }: ActionFunctionArgs) {
6161
}
6262

6363

64-
await db.delete(regex_link).where(eq(regex_link.rxl_id, rxl_id));
64+
await dborm.delete(regex_link).where(eq(regex_link.rxl_id, rxl_id));
6565

6666
//LATER: flash success
6767
return redirect("/links/");

app/routes/links.edit[.]html.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remi
22
import { json, Link as RemixLink, redirect, useLoaderData } from "@remix-run/react";
33
import { eq } from "drizzle-orm"
44

5-
import { db } from "~/db/connection.server";
5+
import { dborm } from "~/db/connection.server";
66
import { regex_link } from "~/db/schema";
77
import { authenticator } from "~/services/auth.server";
88
import { getFormString } from "~/util/getFormString";
@@ -22,7 +22,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
2222
return redirect("/links/");
2323
}
2424

25-
const links = await db.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
25+
const links = await dborm.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
2626
if (!links || links.length != 1) {
2727
//LATER: flash error
2828
return redirect("/links/");
@@ -50,7 +50,7 @@ export async function action({ request }: ActionFunctionArgs) {
5050
}
5151

5252
console.log("updating link 2", rxl_id, JSON.stringify(formData));
53-
const links = await db.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
53+
const links = await dborm.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
5454
if (!links || links.length != 1) {
5555
//LATER: flash error
5656
return redirect("/links/");
@@ -65,10 +65,12 @@ export async function action({ request }: ActionFunctionArgs) {
6565

6666
console.log("updating link 4", rxl_id, getFormString(formData.get("rxl_title")));
6767

68-
await db.update(regex_link).set({
68+
const tags = getFormString(formData.get("rxl_tags")).split(' ').map(tag => tag.trim()).filter(tag => tag != "");
69+
70+
await dborm.update(regex_link).set({
6971
rxl_url: getFormString(formData.get("rxl_url")),
7072
rxl_title: getFormString(formData.get("rxl_title")),
71-
rxl_tags: getFormString(formData.get("rxl_tags")).split(' '),
73+
rxl_tags: tags,
7274
rxl_updated_at: new Date(),
7375
}).where(eq(regex_link.rxl_id, rxl_id));
7476

app/routes/links.tags[.]html.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { MetaFunction } from "@remix-run/node";
2+
import { json, Link as RemixLink, useLoaderData, useSearchParams } from "@remix-run/react";
3+
import { dbconnection } from "~/db/connection.server";
4+
import { TagTree, TagTreeEntry } from "~/components/TagTree";
5+
6+
7+
export const meta: MetaFunction = () => {
8+
return [
9+
{ title: "Links by Tag - Regex Zone" },
10+
];
11+
};
12+
13+
export async function loader() {
14+
15+
const taglinks = await dbconnection`SELECT rxl_id, rxl_title, rxl_url, UNNEST(rxl_tags) as tag FROM regex_link ORDER BY tag`;
16+
17+
console.log(taglinks);
18+
19+
const tagmap: { [key: string]: TagTreeEntry[]; } = {};
20+
for (const taglink of taglinks) {
21+
const tag = taglink.tag;
22+
let links = tagmap[tag];
23+
if (!links) {
24+
links = [];
25+
tagmap[tag] = links;
26+
}
27+
links.push({ id: taglink.rxl_id, title: taglink.rxl_title, url: taglink.rxl_url });
28+
}
29+
30+
console.log(tagmap);
31+
32+
return json(tagmap);
33+
}
34+
35+
export default function Tags() {
36+
const tagMap = useLoaderData<typeof loader>();
37+
const [searchParams] = useSearchParams();
38+
const currentTag = searchParams.get("tag") || "";
39+
40+
return (
41+
<>
42+
<h1 className="py-2">Links by Tag</h1>
43+
{TagTree(currentTag, tagMap)}
44+
<RemixLink to="/links/untagged.html" className="btn btn-primary">Untagged</RemixLink>
45+
</>
46+
);
47+
48+
}

app/routes/links.untagged[.]html.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
2+
import { json } from "@remix-run/react";
3+
import { sql } from "drizzle-orm"
4+
5+
import { cookieStorage } from "~/services/session.server";
6+
import { dborm } from "~/db/connection.server";
7+
import { regex_link } from "~/db/schema";
8+
import { authenticator } from "~/services/auth.server";
9+
import Index from "./links._index"
10+
11+
export const meta: MetaFunction = () => {
12+
return [
13+
{ title: "Untagged Links - Regex Zone" },
14+
{ name: "robots", content: "noindex" },
15+
];
16+
};
17+
18+
export async function loader({ request }: LoaderFunctionArgs) {
19+
// Retrieves the current session from the incoming request's Cookie header
20+
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
21+
22+
// Retrieve the session value set in the previous request
23+
const message = session.get("message");
24+
25+
const links = await dborm.select().from(regex_link).where(sql`ARRAY_LENGTH(${regex_link.rxl_tags}, 1) IS NULL`);
26+
console.log("untagged links", JSON.stringify(links));
27+
const user = authenticator.isAuthenticated(request);
28+
29+
30+
// Commit the session and return the message
31+
return json(
32+
{ links, message, user },
33+
{
34+
headers: {
35+
"Set-Cookie": await cookieStorage.commitSession(session),
36+
},
37+
}
38+
);
39+
}
40+
41+
export default Index;

0 commit comments

Comments
 (0)