Avatar

iamseeley

26 public vals
Joined April 1, 2024

A list of all the vals used in my hono + htmx site

~ thanks to @mxdvl for making the val to show many vals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import { alias } from "https://esm.town/v/neverstew/alias";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo";
import { html } from "https://esm.town/v/pomdtr/gfm";
import { readme } from "https://esm.town/v/pomdtr/readme";
const vals = await Promise.all(
[
"iamseeley/Server2",
"iamseeley/RootLayout",
"iamseeley/LandingPage",
"iamseeley/EditProfilePage",
"iamseeley/ProfilePage",
"iamseeley/SubmitSignup",
"iamseeley/SubmitLogin",
"iamseeley/SignupModal",
"iamseeley/LoginModal",
"iamseeley/AddLink",
"iamseeley/EditLinkModal",
"iamseeley/Queries",
"iamseeley/profileHandlers",
"iamseeley/linkHandlers",
"iamseeley/UserLinks",
"iamseeley/UserProfileHeader",
"iamseeley/Nav",
"iamseeley/Header"
]
.map(async id => {
const [username, valName] = id.split("/");
return alias({ username, valName });
}),
);
export default async function(req: Request) {
return new Response(
`
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HonoHTMX Val</title>
<style>${css}</style>
<link rel="preconnect" href="https://fonts.gstatic.com">
<!-- IBM Plex Sans -->
<link rel="stylesheet" href="https://static.esm.town/build/_assets/400-X2O3NP5A.css">
<link rel="stylesheet" href="https://static.esm.town/build/_assets/400-italic-MXNJ43MZ.css">
<link rel="stylesheet" href="https://static.esm.town/build/_assets/600-FEUL63OX.css">
<link rel="stylesheet" href="https://static.esm.town/build/_assets/700-5YNEZ3QH.css">
<link rel="stylesheet" href="https://static.esm.town/build/_assets/700-italic-RWWCWQ6V.css">
<link rel="stylesheet" href="https://static.esm.town/build/_assets/700-RV7MGDVG.css">
</head>
<body>
<h1>
hono 🤝 htmx 🤝 val town
</h1>
<ul>
${
vals.map((val) => {
const readmeContent = val.readme ? html(val.readme) : "";
return `<li>
${readmeContent}
<details>
<summary><span>${val.author.username}/${val.name} (v${val.version})</span><hr><span>${heart}${val.likeCount} ${ref}${val.referenceCount}</span></summary>
<iframe src="https://www.val.town/embed/${val.author.username}/${val.name}"></iframe>
</details>
</li>`;
}).join("\n")
}
</ul>
</body>
</html>`,
{
headers: {
"Content-Type": "text/html; charset=utf-8",
},
},
);
}
const heart =
`<svg viewBox="0 0 24 24"><path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322
const ref =
`<svg viewBox="0 0 24 24"><path d="M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 1 0-2.636 6.364M16.5 12V8.25" /></svg>`;
const css = `
html { padding: 0.5em; }
body { font-family: IBM Plex Sans, system-ui; max-width: 56rem; margin: 0 auto; }
h1 { padding-top: 0.5em; }
ul { list-style-type: none; padding: 0; }
li { background: rgb(249 250 251); padding: 0.5em; border-radius: 0.25em; border: 1px solid #e5e7eb; }
li + li { margin-block: 1rem; }
li blockquote { font-weight: 500; font-style: italic; border-left: 0.25rem solid rgb(229 231 235); margin-block: 1.6em; margin-inline: 0; padding-left: 1em; }
summary { cursor: pointer; display: flex; align-items: center; }
summary hr { flex-grow: 96; border: none; }
summary svg { height: 0.875em; position: relative; top: 0.125em; stroke: currentColor; stroke-width: 2; fill: none; }
iframe { border: none; min-height: 24rem; width: 100% }`;

🚧 hono + htmx web app 🚧

idea: linktree-esque profile page w/ widgets powered by vals

setup:

  • fork the val and uncomment the /signup and /login routes
  • create a jwt secret token environment variable
  • go to the db setup val and run it to create the tables (as the site is right now, you can only add/edit users and add/edit/delete user links)

to do:

  • create some val town apis for the profile widgets (add vals people have already made)
  • add profile image (will probably point to val town profile pic)
  • add delete profile handler
  • finish public profile page
  • 🎨🎨🎨🎨🎨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from 'https://deno.land/x/hono/middleware.ts';
import { Hono } from "npm:hono@3";
import { sqlite } from "https://esm.town/v/std/sqlite";
import { email } from "https://esm.town/v/std/email?v=11";
import { jwt } from 'npm:hono/jwt';
import RootLayout from "https://esm.town/v/iamseeley/RootLayout";
import LandingPage from "https://esm.town/v/iamseeley/LandingPage";
import EditProfilePage from "https://esm.town/v/iamseeley/EditProfilePage";
import ProfilePage from "https://esm.town/v/iamseeley/ProfilePage";
import SubmitSignup from "https://esm.town/v/iamseeley/SubmitSignup";
import SubmitLogin from "https://esm.town/v/iamseeley/SubmitLogin";
import { logger } from 'npm:hono/logger';
import SignupModal from "https://esm.town/v/iamseeley/SignupModal";
import LoginModal from "https://esm.town/v/iamseeley/LoginModal";
import AddLink from "https://esm.town/v/iamseeley/AddLink";
import EditLinkModal from "https://esm.town/v/iamseeley/EditLinkModal";
import { getLinkById, getUserByUsername } from "https://esm.town/v/iamseeley/Queries";
import { getUserProfileHandler, updateUserProfileHandler } from "https://esm.town/v/iamseeley/profileHandlers";
import { addLinkHandler, updateLinkHandler, deleteLinkHandler } from "https://esm.town/v/iamseeley/linkHandlers";
const app = new Hono();
app.use(logger());
const SECRET_KEY = Deno.env.get("JWT_SECRET_TOKEN");
if (!SECRET_KEY) {
console.error("JWT_SECRET_TOKEN is not set");
}
const jwtMiddleware = jwt({ secret: SECRET_KEY, cookie: 'token' });
app.get('/', (c) => c.html(<LandingPage title="link tree alt" description="simple link tree" />));
// user routes
// app.post('/signup', SubmitSignup);
// app.post('/login', SubmitLogin);
// app.delete('/users/:userId', jwtMiddleware, deleteUserHandler);
// link routes
app.get('/links/:linkId', jwtMiddleware, async (c) => {
const linkId = parseInt(c.req.param('linkId'), 10);
const link = await getLinkById(linkId);
return c.json(link);
});
app.put('/links/:linkId', jwtMiddleware, updateLinkHandler);
app.post('/links', jwtMiddleware, addLinkHandler);
app.delete('/links/:linkId', jwtMiddleware, deleteLinkHandler);
// user profile routes
app.get('/edit-profile/:username', jwtMiddleware, getUserProfileHandler);
app.post('/edit-profile/:username', jwtMiddleware, updateUserProfileHandler);
// app.get('/:username', userProfilePageHandler);
// components (htmx)
app.get('/signupModal', async (c) => c.html(<SignupModal />));
app.get('/loginModal', async (c) => c.html(<LoginModal />));
app.get('/addLinkModal/:username', async (c) => {
const username = c.req.param('username');
return c.html(<AddLink username={username} />);
});
app.get('/edit-link-modal/:username/:linkId', async (c) => {
const username = c.req.param('username');
const linkId = parseInt(c.req.param('linkId'), 10);
const link = await getLinkById(linkId);
return c.html(
<EditLinkModal
username={username}
linkId={link.id}
label={link.label}
url={link.url}
/>);
});
export default app.fetch;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from 'https://deno.land/x/hono/middleware.ts'
import Mapbox from 'npm:mapbox-gl';
interface MapboxProps {
city: string;
}
export default function MapboxComponent({ city }: MapboxProps) {
const mapContainer = <div id="map" style={{ width: '100%', height: '400px' }}></div>;
const initializeMap = () => {
Mapbox.accessToken = 'MAPBOX_ACCESS_TOKEN';
const map = new Mapbox.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
center: [0, 0],
zoom: 2
});
fetch(`https://iamseeley-Map.web.val.run?city=${encodeURIComponent(city)}`)
.then(response => response.json())
.then(mapData => {
if (mapData.error) {
console.error('Error:', mapData.error);
return;
}
map.setCenter(mapData.center);
map.setZoom(mapData.zoom);
mapData.markers.forEach(marker => {
new Mapbox.Marker()
.setLngLat(marker.coordinates)
.setPopup(new Mapbox.Popup().setText(`${marker.title}: ${marker.description}`))
.addTo(map);
});
});
};
setTimeout(initializeMap, 0);
return mapContainer;
}
1
2
3
4
5
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from 'https://deno.land/x/hono/middleware.ts'
export default function ValCommits
1
2
3
4
5
6
7
8
9
10
11
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from 'https://deno.land/x/hono/middleware.ts'
export defualt funtion SpotifyListening({}) {
return (
<>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from 'https://deno.land/x/hono/middleware.ts'
export default function GithubCommits({}) {
return (
<>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from "https://deno.land/x/hono/middleware.ts";
import { getUserByUsername, insertUserLink, deleteUserLink, getLinkById, updateUserLink, getUserLinks } from "https://esm.town/v/iamseeley/Queries";
import EditProfilePage from "https://esm.town/v/iamseeley/EditProfilePage";
import RootLayout from "https://esm.town/v/iamseeley/RootLayout";
import UserLinks from "https://esm.town/v/iamseeley/UserLinks"
export const addLinkHandler = async (c) => {
try {
const payload = await c.get('jwtPayload');
const body = await c.req.parseBody();
const label = body.label;
const url = body.url;
if (!label || !url) {
return c.json({ error: 'Label and URL are required' }, 400);
}
await insertUserLink(payload.userId, label, url);
const username = payload.username;
const userProfile = await getUserByUsername(username);
return c.html(
<EditProfilePage
user={userProfile}
/>
);
} catch (error) {
console.error('Error adding link:', error);
return c.json({ error: 'Error adding link', message: error.message }, 500);
}
};
export const updateLinkHandler = async (c) => {
try {
const payload = await c.get('jwtPayload');
const username = payload.username;
const linkId = parseInt(c.req.param('linkId'), 10);
const body = await c.req.parseBody();
if (!body.label || !body.url) {
return c.json({ error: 'Label and URL are required' }, 400);
}
const link = await getLinkById(linkId);
if (link.userId !== payload.userId) {
return c.json({ error: 'Unauthorized', message: 'This link does not belong to you' }, 403);
}
await updateUserLink(linkId, body.label, body.url);
const userProfile = await getUserByUsername(username);
return c.html(
<EditProfilePage
user={userProfile}
/>
);
} catch (error) {
console.error('Error updating link:', error);
return c.json({ error: 'Error updating link', message: error.message }, 500);
}
};
export const deleteLinkHandler = async (c) => {
try {
const payload = await c.get('jwtPayload');
const username = payload.username;
const linkId = parseInt(c.req.param('linkId'), 10);
await deleteUserLink(linkId, payload.userId);
const userProfile = await getUserByUsername(username);
return c.html(
<EditProfilePage
user={userProfile}
/>
);
} catch (error) {
console.error('Error deleting link:', error);
return c.json({ error: 'Error deleting link', message: error.message }, 500);
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from "https://deno.land/x/hono/middleware.ts";
import { getUserByUsername, updateUser } from "https://esm.town/v/iamseeley/Queries";
import EditProfilePage from "https://esm.town/v/iamseeley/EditProfilePage";
import RootLayout from "https://esm.town/v/iamseeley/RootLayout";
export const getUserProfileHandler = async (c) => {
const username = c.req.param('username');
const payload = await c.get('jwtPayload');
if (payload.username !== username) {
return c.html(
<RootLayout>
<p>You are not authorized to access this profile.</p>
</RootLayout>
);
}
try {
const user = await getUserByUsername(username);
return c.html(<EditProfilePage user={user} />);
} catch (error) {
console.error("Error fetching user:", error);
return c.html(
<RootLayout>
<p>Error loading profile.</p>
</RootLayout>
);
}
};
export const updateUserProfileHandler = async (c) => {
try {
const body = await c.req.parseBody();
const username = c.req.param("username");
const name = body.name;
const bio = body.bio;
if (typeof name !== "string" || typeof bio !== "string") {
throw new Error("name and bio must be strings.");
}
const existingUser = await getUserByUsername(username);
await updateUser(existingUser.id, name, bio);
return c.redirect(`/edit-profile/${username}`);
} catch (error) {
console.error("Error updating profile:", error);
return c.json({ success: false, message: "Error updating profile" });
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/** @jsx jsx */
/** @jsxFrag Fragment */
import { jsx, Fragment } from 'https://deno.land/x/hono/middleware.ts'
export default function EditLinkModal({ username, linkId, label, url }) {
return (
<div id="edit-link-modal" className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white rounded-lg shadow-xl p-8 w-full max-w-md mx-4 mt-4 mb-0">
<h2 className="text-xl font-bold mb-4">Edit Link</h2>
<form action={`/links/${linkId}`} method="put" className="space-y-4" hx-push-url={`/edit-profile/${username}`}>
<input
type="text"
name="label"
value={label}
placeholder="Link Label"
className="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
name="url"
value={url}
placeholder="Link URL"
className="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
>
Update
</button>
<button
type="button"
className="w-full bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded"
hx-delete={`/links/${linkId}`}
hx-target="body"
>
Delete
</button>
</form>
<button
className="w-full bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded"
onclick={"event.preventDefault(); document.getElementById('edit-link-modal').style.display='none';"}
>
Cancel
</button>
</div>
</div>
);
}