···
1
1
import { eq, desc, and, or } from "drizzle-orm";
2
2
import { db } from "../../../libs/db";
3
3
-
import { takes as takesTable } from "../../../libs/schema";
3
3
+
import { takes as takesTable, users as usersTable } from "../../../libs/schema";
4
4
import { handleApiError } from "../../../libs/apiError";
5
5
6
6
export type RecentTake = {
···
10
10
createdAt: Date;
11
11
mediaUrls: string[];
12
12
elapsedTimeMs: number;
13
13
+
project: string;
14
14
+
totalTakesTime: number;
13
15
};
14
16
15
17
export async function recentTakes(url: URL): Promise<Response> {
16
18
try {
17
19
const userId = url.searchParams.get("user");
18
20
19
19
-
const query = db
21
21
+
if (userId) {
22
22
+
// Verify user exists if userId provided
23
23
+
const user = await db
24
24
+
.select()
25
25
+
.from(usersTable)
26
26
+
.where(eq(usersTable.id, userId))
27
27
+
.limit(1);
28
28
+
29
29
+
if (user.length === 0) {
30
30
+
return new Response(
31
31
+
JSON.stringify({
32
32
+
error: "User not found",
33
33
+
takes: [],
34
34
+
}),
35
35
+
{
36
36
+
status: 404,
37
37
+
headers: {
38
38
+
"Content-Type": "application/json",
39
39
+
},
40
40
+
},
41
41
+
);
42
42
+
}
43
43
+
}
44
44
+
45
45
+
const recentTakes = await db
20
46
.select()
21
47
.from(takesTable)
22
48
.orderBy(desc(takesTable.createdAt))
23
49
.where(eq(takesTable.userId, userId ? userId : takesTable.userId))
24
50
.limit(40);
25
25
-
26
26
-
const recentTakes = await query;
27
51
28
52
if (recentTakes.length === 0) {
29
53
return new Response(
···
38
62
);
39
63
}
40
64
65
65
+
// Get unique user IDs
66
66
+
const userIds = [...new Set(recentTakes.map((take) => take.userId))];
67
67
+
68
68
+
// Query users from takes table
69
69
+
const users = await db
70
70
+
.select()
71
71
+
.from(usersTable)
72
72
+
.where(or(...userIds.map((id) => eq(usersTable.id, id))));
73
73
+
74
74
+
// Create map of user data by ID
75
75
+
const userMap = users.reduce(
76
76
+
(acc, user) => {
77
77
+
acc[user.id] = user;
78
78
+
return acc;
79
79
+
},
80
80
+
{} as Record<string, (typeof users)[number]>,
81
81
+
);
82
82
+
41
83
const takes: RecentTake[] =
42
84
recentTakes.map((take) => ({
43
85
id: take.id,
···
46
88
createdAt: new Date(take.createdAt),
47
89
mediaUrls: take.media ? JSON.parse(take.media) : [],
48
90
elapsedTimeMs: take.elapsedTimeMs,
91
91
+
project: userMap[take.userId]?.projectName || "unknown project",
92
92
+
totalTakesTime:
93
93
+
userMap[take.userId]?.totalTakesTime || take.elapsedTimeMs,
49
94
})) || [];
50
95
51
96
return new Response(
···
34
34
35
35
useEffect(() => {
36
36
async function getTakes() {
37
37
-
const res = await fetch("/api/recentTakes");
38
38
-
const data = await res.json();
39
39
-
40
40
-
console.log(data);
41
41
-
setTakes(data.takes);
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
+
}
42
48
}
43
49
getTakes();
44
50
}, []);
···
53
59
return (
54
60
<div className="container">
55
61
<h1 className="title">Recent Takes</h1>
56
56
-
<Masonry
57
57
-
breakpointCols={breakpointColumns}
58
58
-
className="takes-grid"
59
59
-
columnClassName="takes-grid-column"
60
60
-
>
61
61
-
{takes.map((take) => (
62
62
-
<div key={take.id} className="take-card">
63
63
-
<div className="take-header">
64
64
-
<h2 className="take-title">{take.notes}</h2>
65
65
-
<div className="user-pill">
66
66
-
<div className="user-info">
67
67
-
<img
68
68
-
src={userData[take.userId]?.imageUrl}
69
69
-
alt="Profile"
70
70
-
className="profile-image"
71
71
-
/>
72
72
-
<span className="user-name">
73
73
-
{userData[take.userId]?.displayName ??
74
74
-
take.userId}
75
75
-
</span>
62
62
+
{takes.length === 0 ? (
63
63
+
<div className="no-takes-message">No takes found</div>
64
64
+
) : (
65
65
+
<Masonry
66
66
+
breakpointCols={breakpointColumns}
67
67
+
className="takes-grid"
68
68
+
columnClassName="takes-grid-column"
69
69
+
>
70
70
+
{takes.map((take) => (
71
71
+
<div key={take.id} className="take-card">
72
72
+
<div className="take-header">
73
73
+
<h2 className="take-title">{take.project}</h2>
74
74
+
<div className="user-pill">
75
75
+
<div className="user-info">
76
76
+
<img
77
77
+
src={
78
78
+
userData[take.userId]?.imageUrl
79
79
+
}
80
80
+
alt="Profile"
81
81
+
className="profile-image"
82
82
+
/>
83
83
+
<span className="user-name">
84
84
+
{userData[take.userId]
85
85
+
?.displayName ?? take.userId}
86
86
+
</span>
87
87
+
</div>
76
88
</div>
77
89
</div>
78
78
-
</div>
79
90
80
80
-
<div className="take-meta">
81
81
-
<div className="meta-item">
82
82
-
<span className="meta-label">Completed:</span>
83
83
-
<span className="meta-value">
84
84
-
{new Date(take.createdAt).toLocaleString()}
85
85
-
</span>
86
86
-
</div>
87
87
-
<div className="meta-item">
88
88
-
<span className="meta-label">Duration:</span>
89
89
-
<span className="meta-value">
90
90
-
{prettyPrintTime(take.elapsedTimeMs)}
91
91
-
</span>
91
91
+
<div className="take-meta">
92
92
+
<div className="meta-item">
93
93
+
<span className="meta-label">
94
94
+
Completed:
95
95
+
</span>
96
96
+
<span className="meta-value">
97
97
+
{new Date(
98
98
+
take.createdAt,
99
99
+
).toLocaleString()}
100
100
+
</span>
101
101
+
</div>
102
102
+
<div className="meta-item">
103
103
+
<span className="meta-label">
104
104
+
Duration:
105
105
+
</span>
106
106
+
<span className="meta-value">
107
107
+
{prettyPrintTime(take.elapsedTimeMs)}
108
108
+
</span>
109
109
+
</div>
92
110
</div>
93
93
-
</div>
94
111
95
95
-
{take.mediaUrls?.map((url: string, index: number) => {
96
96
-
// More robust video detection for Slack-style URLs
97
97
-
const isVideo =
98
98
-
/\.(mp4|mov|webm|ogg)/i.test(url) ||
99
99
-
(url.includes("files.slack.com") &&
100
100
-
url.includes("download"));
101
101
-
const contentType = isVideo ? "video" : "image";
112
112
+
{take.mediaUrls?.map(
113
113
+
(url: string, index: number) => {
114
114
+
// More robust video detection for Slack-style URLs
115
115
+
const isVideo =
116
116
+
/\.(mp4|mov|webm|ogg)/i.test(url) ||
117
117
+
(url.includes("files.slack.com") &&
118
118
+
url.includes("download"));
119
119
+
const contentType = isVideo
120
120
+
? "video"
121
121
+
: "image";
102
122
103
103
-
return (
104
104
-
<div
105
105
-
key={`media-${take.id}-${index}`}
106
106
-
className={`${contentType}-container`}
107
107
-
>
108
108
-
{isVideo ? (
109
109
-
<video
110
110
-
controls
111
111
-
className="take-video"
112
112
-
preload="metadata"
113
113
-
playsInline
123
123
+
return (
124
124
+
<div
125
125
+
key={`media-${take.id}-${index}`}
126
126
+
className={`${contentType}-container`}
114
127
>
115
115
-
<source
116
116
-
src={url}
117
117
-
type="video/mp4"
118
118
-
/>
119
119
-
<track
120
120
-
kind="captions"
121
121
-
src=""
122
122
-
label="Captions"
123
123
-
/>
124
124
-
Your browser does not support the
125
125
-
video tag.
126
126
-
</video>
127
127
-
) : (
128
128
-
<img
129
129
-
src={url}
130
130
-
alt={`Media content ${index + 1}`}
131
131
-
className="take-image"
132
132
-
loading="lazy"
133
133
-
/>
134
134
-
)}
135
135
-
</div>
136
136
-
);
137
137
-
})}
138
138
-
</div>
139
139
-
))}
140
140
-
</Masonry>
128
128
+
{isVideo ? (
129
129
+
<video
130
130
+
controls
131
131
+
className="take-video"
132
132
+
preload="metadata"
133
133
+
playsInline
134
134
+
>
135
135
+
<source
136
136
+
src={url}
137
137
+
type="video/mp4"
138
138
+
/>
139
139
+
<track
140
140
+
kind="captions"
141
141
+
src=""
142
142
+
label="Captions"
143
143
+
/>
144
144
+
Your browser does not
145
145
+
support the video tag.
146
146
+
</video>
147
147
+
) : (
148
148
+
<img
149
149
+
src={url}
150
150
+
alt={`Media content ${index + 1}`}
151
151
+
className="take-image"
152
152
+
loading="lazy"
153
153
+
/>
154
154
+
)}
155
155
+
</div>
156
156
+
);
157
157
+
},
158
158
+
)}
159
159
+
</div>
160
160
+
))}
161
161
+
</Masonry>
162
162
+
)}
141
163
</div>
142
164
);
143
165
}
···
14
14
max-width: 1200px;
15
15
margin: 0 auto;
16
16
padding: 2rem;
17
17
+
/* Add these properties for vertical centering when there are no takes */
18
18
+
min-height: calc(100vh - 40px); /* 40px accounts for the body padding */
19
19
+
display: flex;
20
20
+
flex-direction: column;
21
21
+
}
22
22
+
23
23
+
.no-takes-message {
24
24
+
text-align: center;
25
25
+
font-size: 1.5rem;
26
26
+
padding: 2rem;
27
27
+
margin: auto; /* This will center vertically when parent is flex */
28
28
+
max-width: 600px;
17
29
}
18
30
19
31
.title {
···
1
1
+
import { eq } from "drizzle-orm";
1
2
import { slackApp, slackClient } from "../../../index";
2
3
import { db } from "../../../libs/db";
3
3
-
import { takes as takesTable } from "../../../libs/schema";
4
4
+
import { takes as takesTable, users as usersTable } from "../../../libs/schema";
4
5
import * as Sentry from "@sentry/bun";
5
6
6
7
export default async function upload() {
···
14
15
payload.channel !== process.env.SLACK_LISTEN_CHANNEL
15
16
)
16
17
return;
18
18
+
19
19
+
const userInDB = await db
20
20
+
.select()
21
21
+
.from(usersTable)
22
22
+
.where(eq(usersTable.id, user));
23
23
+
24
24
+
if (userInDB.length === 0) {
25
25
+
await slackClient.chat.postMessage({
26
26
+
channel: payload.channel,
27
27
+
thread_ts: payload.ts,
28
28
+
text: "we don't have a project for you; set one up in the web ui or by running `/takes`",
29
29
+
});
30
30
+
return;
31
31
+
}
17
32
18
33
// Convert Slack formatting to markdown
19
34
const replaceUserMentions = async (text: string) => {
···
1
1
-
import { pgTable, text, integer } from "drizzle-orm/pg-core";
1
1
+
import { pgTable, text, integer, boolean } from "drizzle-orm/pg-core";
2
2
import type { Pool } from "pg";
3
3
4
4
// Define the takes table
···
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
+
usingHackatimeV2: boolean().notNull().default(true),
24
25
});
25
26
26
27
export async function setupTriggers(pool: Pool) {