Skip to content

Commit 539d91f

Browse files
committed
Links archive and polishing
1 parent 213d0ca commit 539d91f

14 files changed

+256
-88
lines changed

app/components/ItemLinks.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ type ItemLinksProps = {
2424
type: ItemType;
2525
id: string;
2626
adminOnly?: boolean;
27+
currentUrl?: string;
2728
}
2829

29-
export function ItemLinks({ id, type, adminOnly }: ItemLinksProps) {
30+
export function ItemLinks({ currentUrl, id, type, adminOnly }: ItemLinksProps) {
3031
const typeInfo = itemTypeMap[type];
32+
const currentUrlParam = currentUrl ? `&next=${encodeURIComponent(currentUrl)}` : "";
3133
return (
3234
<>
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+
<RemixLink to={`${typeInfo.baseUrl}edit.html?${typeInfo.column_prefix}id=${id}${currentUrlParam}`} className="btn btn-sm btn-secondary mx-1">{ adminOnly ? <AdminIcon /> : null } Edit</RemixLink>
36+
<RemixLink to={`${typeInfo.baseUrl}delete.html?${typeInfo.column_prefix}id=${id}${currentUrlParam}`} className="btn btn-sm btn-secondary mx-1">{adminOnly ? <AdminIcon /> : null} Delete</RemixLink>
3537
</>
3638
);
3739
}

app/components/LinksTable.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
import { LinkTagUrlBuilder } from "~/util/LinkTagUrlBuilder";
3+
import { TagList } from "~/components/TagList";
4+
import { getLinkDomain } from "~/util/getLinkDomain";
5+
import { ItemLinks } from "~/components/ItemLinks";
6+
import { regex_link } from "~/db/schema";
7+
8+
type LinkTableProps = {
9+
links: typeof regex_link.$inferSelect[],
10+
currentUrl: string,
11+
isAdmin?: boolean,
12+
};
13+
14+
export default function LinksTable({ links, currentUrl, isAdmin }: LinkTableProps) {
15+
16+
return (
17+
<>
18+
<table className="table table-striped table-hover border-top">
19+
<tbody>
20+
{links.map(link => (
21+
<tr key={link.rxl_id}>
22+
<td>
23+
<a className="me-2" href={link.rxl_url}>{link.rxl_title}</a>
24+
({getLinkDomain(link.rxl_url)})
25+
</td>
26+
<td className="text-end">
27+
<TagList tags={link.rxl_tags.sort()} urlBuilder={LinkTagUrlBuilder} />
28+
{isAdmin ? <ItemLinks adminOnly={true} currentUrl={currentUrl} type="link" id={link.rxl_id} /> : null}
29+
</td>
30+
</tr>
31+
))}
32+
</tbody>
33+
</table>
34+
</>
35+
);
36+
}

app/components/Tag.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ export function Tag({ tag, url }: TagProps) {
1010
<RemixLink
1111
key={tag}
1212
to={url}
13+
preventScrollReset={true}
1314
className="badge text-bg-primary text-decoration-none me-2"
1415
>
15-
{tag.replace('-', ' ')}
16+
{tag.replaceAll('-', ' ')}
1617
</RemixLink>
1718
);
1819
}

app/components/TagList.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
import { Link as RemixLink } from "@remix-run/react";
1+
import { Tag } from "./Tag";
22

33
type TagListProps = {
44
tags: string[],
55
urlBuilder: (tag: string) => string,
66
}
77

8-
export function TagList( {tags, urlBuilder }: TagListProps) {
8+
export function TagList({ tags, urlBuilder }: TagListProps) {
99
return (
1010
<>
1111
{tags.map((tag) => (
12-
<RemixLink
13-
key={tag}
14-
to={urlBuilder(tag)}
15-
className="badge text-bg-primary text-decoration-none me-2"
16-
>
17-
{tag.replace('-', ' ')}
18-
</RemixLink>
12+
<Tag key={tag} tag={tag} url={urlBuilder(tag)} />
1913
))}
2014
</>
2115
);

app/components/TagTree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type TagTreeEntry = {
1414
function TagTreeRow(tag: string, currentTag: string, entries: SerializeFrom<TagTreeEntry>[]) {
1515
return (
1616
<details className="mt-2" open={tag === currentTag}>
17-
<summary><Tag tag={tag} url={`?tag=${tag}`} /></summary>
17+
<summary><Tag tag={tag} url={`?tag=${encodeURIComponent(tag)}`} /></summary>
1818
<ul className="mt-1">
1919
{entries?.map((entry) => (
2020
<li key={entry.url}>

app/routes/links._index.tsx

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
22
import { json, Link as RemixLink, useLoaderData, useRouteLoaderData } from "@remix-run/react";
3+
import { desc } from "drizzle-orm";
4+
35
import { cookieStorage } from "~/services/session.server";
46
import { User } from "~/types/User";
57
import { AlertWidget } from "~/components/AlertWidget";
68
import type { AlertMessage } from "~/types/AlertMessage";
79
import { dborm } from "~/db/connection.server";
810
import { regex_link } from "~/db/schema";
9-
import { LinkTagUrlBuilder } from "~/util/LinkTagUrlBuilder";
10-
import { TagList } from "~/components/TagList";
1111
import { authenticator } from "~/services/auth.server";
1212
import { AdminIcon } from "~/components/AdminIcon";
13-
import { getLinkDomain } from "~/util/getLinkDomain";
14-
import { ItemLinks } from "~/components/ItemLinks";
13+
import LinksTable from "~/components/LinksTable";
1514

1615
export const meta: MetaFunction = () => {
1716
return [
@@ -28,7 +27,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
2827
const message = session.get("message");
2928
console.log("loader message", JSON.stringify(message));
3029

31-
const links = await dborm.select().from(regex_link);
30+
const links = await dborm.select().from(regex_link).orderBy(desc(regex_link.rxl_created_at)).limit(100);
3231

3332
const user = authenticator.isAuthenticated(request);
3433

@@ -51,44 +50,26 @@ export default function Index() {
5150

5251
const message = data.message as AlertMessage | undefined;
5352

54-
const links = data.links;
53+
const links = data.links as unknown as typeof regex_link.$inferSelect[];
5554

5655
return (
5756
<>
5857
<div className="d-flex justify-content-between align-items-center">
5958
<h1 className="py-2">Links</h1>
60-
{user && user.isAdmin ?
61-
<div>
62-
<RemixLink to="/links/add.html" className="btn btn-primary mx-1"><AdminIcon /> Add</RemixLink>
63-
<RemixLink to="/links/import.html" className="btn btn-primary mx-1"><AdminIcon /> Import</RemixLink>
64-
</div>
65-
: null}
59+
<div>
60+
<RemixLink to="/links/tags.html" className="btn btn-primary mx-1">Tags</RemixLink>
61+
<RemixLink to="/links/archive/" className="btn btn-primary mx-1">Archive</RemixLink>
62+
{user && user.isAdmin ?
63+
<>
64+
<RemixLink to="/links/add.html" className="btn btn-primary mx-1"><AdminIcon /> Add</RemixLink>
65+
<RemixLink to="/links/import.html" className="btn btn-primary mx-1"><AdminIcon /> Import</RemixLink>
66+
</>
67+
: null}
68+
</div>
6669
</div>
6770
{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>
74-
</tr>
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>
91-
}
71+
<LinksTable currentUrl="/links/" links={links} isAdmin={user?.isAdmin} />
72+
<RemixLink to="/links/archive/" className="btn btn-primary">Archive</RemixLink>
9273
</>
9374
);
9475
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
2+
import { json, Link as RemixLink, useLoaderData, useParams, useRouteLoaderData } from "@remix-run/react";
3+
import { desc, sql } from "drizzle-orm";
4+
5+
import { cookieStorage } from "~/services/session.server";
6+
import { User } from "~/types/User";
7+
import { AlertWidget } from "~/components/AlertWidget";
8+
import type { AlertMessage } from "~/types/AlertMessage";
9+
import { dborm } from "~/db/connection.server";
10+
import { regex_link } from "~/db/schema";
11+
import { authenticator } from "~/services/auth.server";
12+
import { AdminIcon } from "~/components/AdminIcon";
13+
import LinksTable from "~/components/LinksTable";
14+
import { PiArrowFatUpBold, PiCaretLeftBold, PiCaretRightBold } from "react-icons/pi";
15+
16+
export const meta: MetaFunction = ({ params }) => {
17+
return [
18+
{ title: `Links Archive for ${params.year} - Regex Zone` },
19+
];
20+
};
21+
22+
const MIN_YEAR = 2007;
23+
24+
export async function loader({ request, params }: LoaderFunctionArgs) {
25+
// Retrieves the current session from the incoming request's Cookie header
26+
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
27+
28+
// Retrieve the session value set in the previous request
29+
const message = session.get("message");
30+
console.log("loader message", JSON.stringify(message));
31+
32+
if (!params || !params.year || !/^2\d{3}$/.test(params.year)) {
33+
throw new Error("Invalid year");
34+
}
35+
36+
const links = await dborm.select()
37+
.from(regex_link)
38+
.where(sql`EXTRACT(YEAR FROM ${regex_link.rxl_created_at}) = ${params.year}`)
39+
.orderBy(desc(regex_link.rxl_created_at));
40+
41+
const user = authenticator.isAuthenticated(request);
42+
43+
44+
// Commit the session and return the message
45+
return json(
46+
{ links, message, user },
47+
{
48+
headers: {
49+
"Set-Cookie": await cookieStorage.commitSession(session),
50+
},
51+
}
52+
);
53+
}
54+
export default function Index() {
55+
const params = useParams();
56+
const user = useRouteLoaderData<User | null>("root");
57+
const data = useLoaderData<typeof loader>();
58+
59+
const currentYear = new Date().getFullYear();
60+
const year = parseInt(params.year || currentYear.toString());
61+
62+
63+
console.log("func message", JSON.stringify(data));
64+
65+
const message = data.message as AlertMessage | undefined;
66+
67+
const links = data.links as unknown as typeof regex_link.$inferSelect[];
68+
69+
return (
70+
<>
71+
<div className="d-flex justify-content-between align-items-center">
72+
<h1 className="py-2">Links Archive for {year}</h1>
73+
<div>
74+
{ year > MIN_YEAR ? <RemixLink to={`/links/archive/${year - 1}/`} className="btn btn-primary mx-1"><PiCaretLeftBold /> {year - 1}</RemixLink> : null }
75+
<RemixLink to="/links/archive/" className="btn btn-primary"><PiArrowFatUpBold /></RemixLink>
76+
{ year < currentYear ? <RemixLink to={`/links/archive/${year + 1}/`} className="btn btn-primary mx-1">{year + 1} <PiCaretRightBold /></RemixLink> : null }
77+
</div>
78+
</div>
79+
{message ? <AlertWidget alert={message} /> : null}
80+
{ links.length == 0
81+
? <AlertWidget alert={{type: "danger", message: `No links for ${year}`}} />
82+
: <LinksTable currentUrl={`/links/archive/${year}/`} links={links} isAdmin={user?.isAdmin} />
83+
}
84+
</>
85+
);
86+
}

app/routes/links.archive._index.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 Archive - Regex Zone" },
10+
];
11+
};
12+
13+
export async function loader() {
14+
15+
const archiveyears = await dbconnection`SELECT EXTRACT(YEAR FROM rxl_created_at) AS year, COUNT(*) as count FROM regex_link GROUP BY EXTRACT(YEAR FROM rxl_created_at) ORDER BY EXTRACT(YEAR FROM rxl_created_at) DESC`;
16+
17+
console.log(archiveyears);
18+
19+
return json(archiveyears);
20+
}
21+
22+
export default function Tags() {
23+
const archiveyears = useLoaderData<typeof loader>();
24+
25+
return (
26+
<>
27+
<h1 className="py-2">Links Archive</h1>
28+
{ archiveyears.map((archiveyear) => (
29+
<div className="mb-3" key={archiveyear.year}>
30+
<RemixLink className="btn btn-primary" to={`/links/archive/${archiveyear.year}/`}>{archiveyear.year}</RemixLink> ({archiveyear.count})
31+
</div>
32+
)) }
33+
</>
34+
);
35+
36+
}

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

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

55
import { dborm } from "~/db/connection.server";
@@ -42,33 +42,35 @@ export async function loader({ request }: LoaderFunctionArgs) {
4242

4343
export async function action({ request }: ActionFunctionArgs) {
4444
const formData = await request.formData();
45+
const next = getFormString(formData.get("next")) || "/links/";
4546
const rxl_id = getFormString(formData.get("rxl_id"));
4647
if (!rxl_id) {
4748
//LATER: flash error
48-
return redirect("/links/");
49+
return redirect(next);
4950
}
5051

5152
const links = await dborm.select().from(regex_link).where(eq(regex_link.rxl_id, rxl_id));
5253
if (!links || links.length != 1) {
5354
//LATER: flash error
54-
return redirect("/links/");
55+
return redirect(next);
5556
}
5657

5758
const user = await authenticator.isAuthenticated(request);
5859
if (!user || !user.isAdmin) {
5960
//LATER: flash error
60-
return redirect("/links/");
61+
return redirect(next);
6162
}
6263

63-
6464
await dborm.delete(regex_link).where(eq(regex_link.rxl_id, rxl_id));
6565

6666
//LATER: flash success
67-
return redirect("/links/");
67+
return redirect(next);
6868
}
6969

7070
export default function Index() {
7171
const data = useLoaderData<typeof loader>();
72+
const [searchParams] = useSearchParams();
73+
const next = searchParams.get("next") || "/links/";
7274

7375
const theLink = data.link;
7476

@@ -77,6 +79,7 @@ export default function Index() {
7779
<h1 className="py-2">Edit Link</h1>
7880
<form method="post">
7981
<input type="hidden" name="rxl_id" value={theLink.rxl_id} />
82+
<input type="hidden" name="next" value={next} />
8083
<div className="mb-3">
8184
<label htmlFor="rxl_url" className="form-label">URL</label>
8285
<input type="text" className="form-control" id="rxl_url" name="rxl_url" defaultValue={theLink.rxl_url} disabled readOnly />
@@ -90,7 +93,7 @@ export default function Index() {
9093
<input type="text" className="form-control" id="rxl_tags" name="rxl_tags" defaultValue={theLink.rxl_tags.join(' ')} disabled readOnly />
9194
</div>
9295
<input type="submit" className="btn btn-primary" value="Delete" />
93-
<RemixLink className="btn btn-outline-primary mx-2" to="/links/">Cancel</RemixLink>
96+
<RemixLink className="btn btn-outline-primary mx-2" to={next}>Cancel</RemixLink>
9497
</form>
9598
</>
9699
);

0 commit comments

Comments
 (0)