tangled
alpha
login
or
join now
dunkirk.sh
/
smokie
1
fork
atom
a fun bot for the hc slack
1
fork
atom
overview
issues
pulls
pipelines
feat: add user creation and frontend
dunkirk.sh
10 months ago
699b0688
d6d6fc83
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+434
-25
15 changed files
expand all
collapse all
unified
split
bun.lock
package.json
src
features
api
index.ts
routes
projects.ts
frontend
App.tsx
index.tsx
pages
404.tsx
ProjectTakes.tsx
Projects.tsx
styles.css
takes
handlers
setup.ts
setup
actions.ts
commands.ts
index.ts
libs
schema.ts
+11
bun.lock
···
15
15
"react": "^19.1.0",
16
16
"react-dom": "^19.1.0",
17
17
"react-masonry-css": "^1.0.16",
18
18
+
"react-router-dom": "^7.5.1",
18
19
"slack-edge": "^1.3.7",
19
20
"yaml": "^2.7.1",
20
21
},
···
198
199
199
200
"colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="],
200
201
202
202
+
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
203
203
+
201
204
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
202
205
203
206
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
···
270
273
271
274
"react-masonry-css": ["react-masonry-css@1.0.16", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ=="],
272
275
276
276
+
"react-router": ["react-router@7.5.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA=="],
277
277
+
278
278
+
"react-router-dom": ["react-router-dom@7.5.1", "", { "dependencies": { "react-router": "7.5.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA=="],
279
279
+
273
280
"require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="],
274
281
275
282
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
···
280
287
281
288
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
282
289
290
290
+
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
291
291
+
283
292
"shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
284
293
285
294
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
···
295
304
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
296
305
297
306
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
307
307
+
308
308
+
"turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
298
309
299
310
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
300
311
+1
package.json
···
32
32
"react": "^19.1.0",
33
33
"react-dom": "^19.1.0",
34
34
"react-masonry-css": "^1.0.16",
35
35
+
"react-router-dom": "^7.5.1",
35
36
"slack-edge": "^1.3.7",
36
37
"yaml": "^2.7.1"
37
38
}
+3
src/features/api/index.ts
···
1
1
import { recentTakes } from "./routes/recentTakes";
2
2
import video from "./routes/video";
3
3
import { handleApiError } from "../../libs/apiError";
4
4
+
import { projects } from "./routes/projects";
4
5
5
6
export { default as video } from "./routes/video";
6
7
···
13
14
return await video(url);
14
15
case "recentTakes":
15
16
return await recentTakes(url);
17
17
+
case "projects":
18
18
+
return await projects(url);
16
19
default:
17
20
return new Response(
18
21
JSON.stringify({ error: "Route not found" }),
+54
src/features/api/routes/projects.ts
···
1
1
+
import { db } from "../../../libs/db";
2
2
+
import { users as usersTable } from "../../../libs/schema";
3
3
+
import { handleApiError } from "../../../libs/apiError";
4
4
+
import { eq } from "drizzle-orm";
5
5
+
6
6
+
export type Project = {
7
7
+
projectName: string;
8
8
+
projectDescription: string;
9
9
+
projectBannerUrl: string;
10
10
+
totalTakesTime: number;
11
11
+
userId: string;
12
12
+
};
13
13
+
14
14
+
export async function projects(url: URL): Promise<Response> {
15
15
+
const user = url.searchParams.get("user");
16
16
+
try {
17
17
+
const projects = await db
18
18
+
.select({
19
19
+
projectName: usersTable.projectName,
20
20
+
projectDescription: usersTable.projectDescription,
21
21
+
projectBannerUrl: usersTable.projectBannerUrl,
22
22
+
totalTakesTime: usersTable.totalTakesTime,
23
23
+
userId: usersTable.id,
24
24
+
})
25
25
+
.from(usersTable)
26
26
+
.where(eq(usersTable.id, user ? user : usersTable.id));
27
27
+
28
28
+
if (projects.length === 0) {
29
29
+
return new Response(
30
30
+
JSON.stringify({
31
31
+
projects: [],
32
32
+
}),
33
33
+
{
34
34
+
headers: {
35
35
+
"Content-Type": "application/json",
36
36
+
},
37
37
+
},
38
38
+
);
39
39
+
}
40
40
+
41
41
+
return new Response(
42
42
+
JSON.stringify({
43
43
+
projects: user ? projects[0] : projects,
44
44
+
}),
45
45
+
{
46
46
+
headers: {
47
47
+
"Content-Type": "application/json",
48
48
+
},
49
49
+
},
50
50
+
);
51
51
+
} catch (error) {
52
52
+
return handleApiError(error, "projects");
53
53
+
}
54
54
+
}
+16
src/features/frontend/App.tsx
···
1
1
+
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
2
2
+
import { Projects } from "./pages/Projects";
3
3
+
import { ProjectTakes } from "./pages/ProjectTakes";
4
4
+
import { NotFound } from "./pages/404";
5
5
+
6
6
+
export function App() {
7
7
+
return (
8
8
+
<Router>
9
9
+
<Routes>
10
10
+
<Route path="/" element={<Projects />} />
11
11
+
<Route path="/user/:user" element={<ProjectTakes />} />
12
12
+
<Route path="*" element={<NotFound />} />
13
13
+
</Routes>
14
14
+
</Router>
15
15
+
);
16
16
+
}
+64
-23
src/features/frontend/app.tsx
src/features/frontend/pages/ProjectTakes.tsx
···
1
1
import { useEffect, useState } from "react";
2
2
-
import { prettyPrintTime } from "../../libs/time";
3
3
-
import { fetchUserData } from "../../libs/cachet";
4
4
-
import type { RecentTake } from "../api/routes/recentTakes";
2
2
+
import { useParams } from "react-router-dom";
3
3
+
import { prettyPrintTime } from "../../../libs/time";
4
4
+
import { fetchUserData } from "../../../libs/cachet";
5
5
+
import type { RecentTake } from "../../api/routes/recentTakes";
6
6
+
import type { Project } from "../../api/routes/projects";
5
7
import Masonry from "react-masonry-css";
6
8
7
7
-
export function App() {
9
9
+
export function ProjectTakes() {
10
10
+
const { user } = useParams();
8
11
const [takes, setTakes] = useState<RecentTake[]>([]);
9
9
-
10
12
const [userData, setUserData] = useState<{
11
13
[key: string]: { displayName: string; imageUrl: string };
12
14
}>({});
15
15
+
const [project, setProject] = useState<Project>();
16
16
+
17
17
+
useEffect(() => {
18
18
+
async function getTakes() {
19
19
+
try {
20
20
+
const res = await fetch(
21
21
+
`/api/recentTakes?user=${encodeURIComponent(user as string)}`,
22
22
+
);
23
23
+
if (!res.ok) {
24
24
+
throw new Error(`HTTP error! status: ${res.status}`);
25
25
+
}
26
26
+
const data = await res.json();
27
27
+
setTakes(data.takes);
28
28
+
} catch (error) {
29
29
+
console.error("Error fetching takes:", error);
30
30
+
setTakes([]);
31
31
+
}
32
32
+
}
33
33
+
34
34
+
async function getProject() {
35
35
+
try {
36
36
+
const res = await fetch(
37
37
+
`/api/projects?user=${encodeURIComponent(user as string)}`,
38
38
+
);
39
39
+
if (!res.ok) {
40
40
+
throw new Error(`HTTP error! status: ${res.status}`);
41
41
+
}
42
42
+
const data = await res.json();
43
43
+
setProject(data.projects);
44
44
+
} catch (error) {
45
45
+
console.error("Error fetching project:", error);
46
46
+
}
47
47
+
}
48
48
+
49
49
+
getTakes();
50
50
+
getProject();
51
51
+
}, [user]);
52
52
+
13
53
useEffect(() => {
14
54
async function loadUserData() {
15
55
const userIds = takes.map((take) => take.userId);
···
32
72
loadUserData();
33
73
}, [takes]);
34
74
35
35
-
useEffect(() => {
36
36
-
async function getTakes() {
37
37
-
try {
38
38
-
const res = await fetch("/api/recentTakes");
39
39
-
if (!res.ok) {
40
40
-
throw new Error(`HTTP error! status: ${res.status}`);
41
41
-
}
42
42
-
const data = await res.json();
43
43
-
setTakes(data.takes);
44
44
-
} catch (error) {
45
45
-
console.error("Error fetching takes:", error);
46
46
-
setTakes([]);
47
47
-
}
48
48
-
}
49
49
-
getTakes();
50
50
-
}, []);
51
51
-
52
75
const breakpointColumns = {
53
76
default: 4,
54
77
1100: 3,
···
58
81
59
82
return (
60
83
<div className="container">
61
61
-
<h1 className="title">Recent Takes</h1>
84
84
+
<section className="project-header">
85
85
+
{project?.projectBannerUrl && (
86
86
+
<img
87
87
+
src={project.projectBannerUrl}
88
88
+
alt="Project banner"
89
89
+
className="project-banner"
90
90
+
style={{
91
91
+
width: "100%",
92
92
+
height: "200px",
93
93
+
objectFit: "cover",
94
94
+
borderRadius: "12px",
95
95
+
marginBottom: "2rem",
96
96
+
}}
97
97
+
/>
98
98
+
)}
99
99
+
<h1 className="title">
100
100
+
{project?.projectName || "Recent Takes"}
101
101
+
</h1>
102
102
+
</section>
62
103
{takes.length === 0 ? (
63
104
<div className="no-takes-message">No takes found</div>
64
105
) : (
+1
-1
src/features/frontend/index.tsx
···
1
1
import "./styles.css";
2
2
import { createRoot } from "react-dom/client";
3
3
-
import { App } from "./app.tsx";
3
3
+
import { App } from "./App.tsx";
4
4
5
5
document.addEventListener("DOMContentLoaded", () => {
6
6
const element = document.getElementById("root");
+33
src/features/frontend/pages/404.tsx
···
1
1
+
import { useNavigate } from "react-router-dom";
2
2
+
import { useEffect, useState } from "react";
3
3
+
4
4
+
export function NotFound() {
5
5
+
const navigate = useNavigate();
6
6
+
const [countdown, setCountdown] = useState(5);
7
7
+
8
8
+
useEffect(() => {
9
9
+
const timer = setInterval(() => {
10
10
+
setCountdown((prev) => {
11
11
+
if (prev <= 1) {
12
12
+
clearInterval(timer);
13
13
+
navigate("/");
14
14
+
return 0;
15
15
+
}
16
16
+
return prev - 1;
17
17
+
});
18
18
+
}, 1000);
19
19
+
20
20
+
return () => clearInterval(timer);
21
21
+
}, [navigate]);
22
22
+
23
23
+
return (
24
24
+
<div className="container">
25
25
+
<h1 className="title">404 - Page Not Found</h1>
26
26
+
<div className="no-takes-message">
27
27
+
<p>Redirecting to home page in {countdown} seconds...</p>
28
28
+
</div>
29
29
+
</div>
30
30
+
);
31
31
+
}
32
32
+
33
33
+
export default NotFound;
+59
src/features/frontend/pages/Projects.tsx
···
1
1
+
import { useEffect, useState } from "react";
2
2
+
import { Link } from "react-router-dom";
3
3
+
import { prettyPrintTime } from "../../../libs/time";
4
4
+
import type { Project } from "../../api/routes/projects";
5
5
+
6
6
+
export function Projects() {
7
7
+
const [projects, setProjects] = useState<Project[]>([]);
8
8
+
9
9
+
useEffect(() => {
10
10
+
async function getProjects() {
11
11
+
try {
12
12
+
const res = await fetch("/api/projects");
13
13
+
if (!res.ok) {
14
14
+
throw new Error(`HTTP error! status: ${res.status}`);
15
15
+
}
16
16
+
const data = await res.json();
17
17
+
setProjects(data.projects);
18
18
+
} catch (error) {
19
19
+
console.error("Error fetching projects:", error);
20
20
+
setProjects([]);
21
21
+
}
22
22
+
}
23
23
+
getProjects();
24
24
+
}, []);
25
25
+
26
26
+
return (
27
27
+
<div className="container">
28
28
+
<h1 className="title">Projects</h1>
29
29
+
{projects.length === 0 ? (
30
30
+
<div className="no-takes-message">No projects found</div>
31
31
+
) : (
32
32
+
<div className="projects-grid">
33
33
+
{projects.map((project) => (
34
34
+
<Link
35
35
+
to={`/user/${encodeURIComponent(project.userId)}`}
36
36
+
key={project.projectName}
37
37
+
className="project-card"
38
38
+
>
39
39
+
<img
40
40
+
src={project.projectBannerUrl}
41
41
+
alt={`${project.projectName} banner`}
42
42
+
className="project-banner"
43
43
+
/>
44
44
+
<h2 className="project-title">
45
45
+
{project.projectName}
46
46
+
</h2>
47
47
+
<div className="project-meta">
48
48
+
<span>
49
49
+
Total Time:{" "}
50
50
+
{prettyPrintTime(project.totalTakesTime)}
51
51
+
</span>
52
52
+
</div>
53
53
+
</Link>
54
54
+
))}
55
55
+
</div>
56
56
+
)}
57
57
+
</div>
58
58
+
);
59
59
+
}
+48
src/features/frontend/styles.css
···
115
115
border-radius: 8px;
116
116
max-height: 40rem;
117
117
}
118
118
+
119
119
+
.projects-grid {
120
120
+
display: grid;
121
121
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
122
122
+
gap: 2rem;
123
123
+
}
124
124
+
125
125
+
.project-card {
126
126
+
background: white;
127
127
+
border-radius: 12px;
128
128
+
padding: 1.5rem;
129
129
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
130
130
+
text-decoration: none;
131
131
+
color: inherit;
132
132
+
transition:
133
133
+
transform 0.2s,
134
134
+
box-shadow 0.2s;
135
135
+
}
136
136
+
137
137
+
.project-card:hover {
138
138
+
transform: translateY(-2px);
139
139
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
140
140
+
}
141
141
+
142
142
+
.project-title {
143
143
+
font-size: 1.5rem;
144
144
+
margin: 0 0 1rem 0;
145
145
+
}
146
146
+
147
147
+
.project-meta {
148
148
+
color: #666;
149
149
+
}
150
150
+
151
151
+
.project-banner {
152
152
+
width: 100%;
153
153
+
height: 200px;
154
154
+
object-fit: cover;
155
155
+
border-radius: 8px;
156
156
+
margin-bottom: 1rem;
157
157
+
}
158
158
+
159
159
+
.project-banner-container {
160
160
+
width: 100%;
161
161
+
margin-bottom: 2rem;
162
162
+
border-radius: 12px;
163
163
+
overflow: hidden;
164
164
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
165
165
+
}
+115
src/features/takes/handlers/setup.ts
···
1
1
+
import type { UploadedFile } from "slack-edge";
2
2
+
import { slackApp, slackClient } from "../../../index";
3
3
+
import { db } from "../../../libs/db";
4
4
+
import { users as usersTable } from "../../../libs/schema";
5
5
+
6
6
+
export async function handleSetup(triggerID: string) {
7
7
+
await slackClient.views.open({
8
8
+
trigger_id: triggerID,
9
9
+
view: {
10
10
+
type: "modal",
11
11
+
title: {
12
12
+
type: "plain_text",
13
13
+
text: "Setup Project",
14
14
+
},
15
15
+
submit: {
16
16
+
type: "plain_text",
17
17
+
text: "Submit",
18
18
+
},
19
19
+
clear_on_close: true,
20
20
+
callback_id: "takes_setup_submit",
21
21
+
blocks: [
22
22
+
{
23
23
+
type: "input",
24
24
+
block_id: "project_name",
25
25
+
label: {
26
26
+
type: "plain_text",
27
27
+
text: "Project Name",
28
28
+
},
29
29
+
element: {
30
30
+
type: "plain_text_input",
31
31
+
action_id: "project_name_input",
32
32
+
placeholder: {
33
33
+
type: "plain_text",
34
34
+
text: "Enter your project name",
35
35
+
},
36
36
+
},
37
37
+
},
38
38
+
{
39
39
+
type: "input",
40
40
+
block_id: "project_description",
41
41
+
label: {
42
42
+
type: "plain_text",
43
43
+
text: "Project Description",
44
44
+
},
45
45
+
element: {
46
46
+
type: "plain_text_input",
47
47
+
action_id: "project_description_input",
48
48
+
multiline: true,
49
49
+
placeholder: {
50
50
+
type: "plain_text",
51
51
+
text: "Describe your project",
52
52
+
},
53
53
+
},
54
54
+
},
55
55
+
{
56
56
+
type: "input",
57
57
+
block_id: "project_banner",
58
58
+
label: {
59
59
+
type: "plain_text",
60
60
+
text: "Project Banner Image",
61
61
+
},
62
62
+
element: {
63
63
+
type: "file_input",
64
64
+
action_id: "project_banner_input",
65
65
+
},
66
66
+
},
67
67
+
],
68
68
+
},
69
69
+
});
70
70
+
}
71
71
+
72
72
+
export async function setupSubmitListener() {
73
73
+
slackApp.view(
74
74
+
"takes_setup_submit",
75
75
+
async () => Promise.resolve(),
76
76
+
async ({ payload, body }) => {
77
77
+
if (payload.type !== "view_submission") return;
78
78
+
const values = payload.view.state.values;
79
79
+
const userId = body.user.id;
80
80
+
81
81
+
const file = values.project_banner?.project_banner_input
82
82
+
?.files?.[0] as UploadedFile;
83
83
+
try {
84
84
+
// If file is already public, use it directly
85
85
+
const fileData = file.is_public
86
86
+
? file
87
87
+
: (
88
88
+
await slackClient.files.sharedPublicURL({
89
89
+
file: file.id,
90
90
+
token: process.env.SLACK_USER_TOKEN,
91
91
+
})
92
92
+
).file;
93
93
+
94
94
+
const html = await (
95
95
+
await fetch(fileData?.permalink_public as string)
96
96
+
).text();
97
97
+
const projectBannerUrl = html.match(
98
98
+
/https:\/\/files.slack.com\/files-pri\/[^"]+pub_secret=([^"&]*)/,
99
99
+
)?.[0];
100
100
+
101
101
+
await db.insert(usersTable).values({
102
102
+
id: userId,
103
103
+
projectName: values.project_name?.project_name_input
104
104
+
?.value as string,
105
105
+
projectDescription: values.project_description
106
106
+
?.project_description_input?.value as string,
107
107
+
projectBannerUrl,
108
108
+
});
109
109
+
} catch (error) {
110
110
+
console.error("Error processing file:", error);
111
111
+
throw error;
112
112
+
}
113
113
+
},
114
114
+
);
115
115
+
}
+12
src/features/takes/setup/actions.ts
···
3
3
import handleHelp from "../handlers/help";
4
4
import { handleHistory } from "../handlers/history";
5
5
import handleHome from "../handlers/home";
6
6
+
import { setupSubmitListener } from "../handlers/setup";
6
7
import upload from "../services/upload";
7
8
import type { MessageResponse } from "../types";
8
9
import * as Sentry from "@sentry/bun";
···
70
71
Sentry.captureException(error, {
71
72
extra: {
72
73
context: "upload setup",
74
74
+
},
75
75
+
});
76
76
+
}
77
77
+
78
78
+
// setup the setup view handler
79
79
+
try {
80
80
+
setupSubmitListener();
81
81
+
} catch (error) {
82
82
+
Sentry.captureException(error, {
83
83
+
extra: {
84
84
+
context: "submit modal setup",
73
85
},
74
86
});
75
87
}
+14
src/features/takes/setup/commands.ts
···
5
5
import * as Sentry from "@sentry/bun";
6
6
import { blog } from "../../../libs/Logger";
7
7
import handleHome from "../handlers/home";
8
8
+
import { db } from "../../../libs/db";
9
9
+
import { users as usersTable } from "../../../libs/schema";
10
10
+
import { eq } from "drizzle-orm";
11
11
+
import { handleSetup } from "../handlers/setup";
8
12
9
13
export default function setupCommands() {
10
14
// Main command handler
···
19
23
const subcommand = args[0]?.toLowerCase() || "";
20
24
21
25
let response: MessageResponse | undefined;
26
26
+
27
27
+
const userFromDB = await db
28
28
+
.select()
29
29
+
.from(usersTable)
30
30
+
.where(eq(usersTable.id, userId));
31
31
+
32
32
+
if (userFromDB.length === 0) {
33
33
+
await handleSetup(context.triggerId as string);
34
34
+
return;
35
35
+
}
22
36
23
37
// Route to the appropriate handler function
24
38
switch (subcommand) {
+1
src/index.ts
···
60
60
development: environment === "dev",
61
61
routes: {
62
62
"/": frontend,
63
63
+
"/user/*": frontend,
63
64
"/health": new Response("OK"),
64
65
},
65
66
async fetch(request: Request) {
+2
-1
src/libs/schema.ts
···
17
17
18
18
export const users = pgTable("users", {
19
19
id: text("id").primaryKey(),
20
20
-
totalTakesTime: integer("total_takes_time").default(0),
20
20
+
totalTakesTime: integer("total_takes_time").default(0).notNull(),
21
21
hackatimeKeys: text("hackatime_keys").notNull().default("[]"),
22
22
projectName: text("project_name").notNull().default(""),
23
23
projectDescription: text("project_description").notNull().default(""),
24
24
+
projectBannerUrl: text("project_banner_url").notNull().default(""),
24
25
usingHackatimeV2: boolean().notNull().default(true),
25
26
});
26
27