Merge pull request #3 from System-End/main

feat: auto-submit projects to ysws thingie on review submission
This commit is contained in:
Nathan 2026-02-17 11:47:57 -05:00 committed by GitHub
commit 7e52a1aa85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 96 additions and 8 deletions

View file

@ -36,5 +36,8 @@ export const config = {
airtableToken: process.env.AIRTABLE_TOKEN,
airtableBaseId: process.env.AIRTABLE_BASE_ID,
airtableProjectsTableId: process.env.AIRTABLE_PROJECTS_TABLE_ID!,
airtableUsersTableId: process.env.AIRTABLE_USERS_TABLE_ID!
airtableUsersTableId: process.env.AIRTABLE_USERS_TABLE_ID!,
// YSWS
fraudToken: process.env.FRAUD_TOKEN
}

54
backend/src/lib/ysws.ts Normal file
View file

@ -0,0 +1,54 @@
import { config } from '../config'
const YSWS_API_URL = 'https://joe.fraud.hackclub.com/api/v1/ysws/events/new-ui-api'
export async function submitProjectToYSWS(project: {
name: string
githubUrl: string | null
playableUrl: string | null
hackatimeProject: string | null
slackId: string | null
}) {
if (!config.fraudToken) {
console.log('[YSWS] Missing FRAUD_TOKEN, skipping submission')
return null
}
const hackatimeProjects = project.hackatimeProject
? project.hackatimeProject.split(',').map(n => n.trim()).filter(n => n.length > 0)
: []
const payload = {
name: project.name,
codeLink: project.githubUrl || '',
demoLink: project.playableUrl || '',
submitter: {
slackId: project.slackId || ''
},
hackatimeProjects
}
try {
const res = await fetch(`${YSWS_API_URL}/projects`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.fraudToken}`
},
body: JSON.stringify(payload)
})
if (!res.ok) {
const text = await res.text()
console.error('[YSWS] submission failed:', res.status, text)
return null
}
const data = await res.json()
console.log('[YSWS] submitted project:', project.name)
return data
} catch (e) {
console.error('[YSWS] submission error:', e)
return null
}
}

View file

@ -775,9 +775,12 @@ admin.get('/second-pass', async ({ headers, query }) => {
const offset = (page - 1) * limit
const sort = (query.sort as string) || 'oldest'
const orderClause = sort === 'newest'
? desc(projectsTable.updatedAt)
: asc(projectsTable.updatedAt)
const submittedAtOrderExpr = sql<Date | null>`COALESCE((
SELECT MAX(pa.created_at)
FROM project_activity pa
WHERE pa.project_id = projects.id
AND pa.action = 'project_submitted'
), ${projectsTable.updatedAt})`
const [projects, countResult] = await Promise.all([
db.select({
@ -804,10 +807,18 @@ admin.get('/second-pass', async ({ headers, query }) => {
feedbackGood: projectsTable.feedbackGood,
feedbackImprove: projectsTable.feedbackImprove,
createdAt: projectsTable.createdAt,
updatedAt: projectsTable.updatedAt
updatedAt: projectsTable.updatedAt,
submittedAt: sql<Date | null>`(
SELECT MAX(pa.created_at)
FROM project_activity pa
WHERE pa.project_id = projects.id
AND pa.action = 'project_submitted'
)`
}).from(projectsTable)
.where(eq(projectsTable.status, 'pending_admin_approval'))
.orderBy(orderClause)
.orderBy(sort === 'newest'
? desc(submittedAtOrderExpr)
: asc(submittedAtOrderExpr))
.limit(limit)
.offset(offset),
db.select({ count: sql<number>`count(*)` }).from(projectsTable)

View file

@ -8,6 +8,7 @@ import { projectActivityTable } from '../schemas/activity'
import { getUserFromSession, fetchUserIdentity } from '../lib/auth'
import { syncSingleProject } from '../lib/hackatime-sync'
import { notifyProjectSubmitted } from '../lib/slack'
import { submitProjectToYSWS } from '../lib/ysws'
import { config } from '../config'
import { computeEffectiveHoursForProject } from '../lib/effective-hours'
@ -643,6 +644,15 @@ projects.post("/:id/submit", async ({ params, headers, body }) => {
action: 'project_submitted'
})
// Submit to YSWS
submitProjectToYSWS({
name: updated[0].name,
githubUrl: updated[0].githubUrl,
playableUrl: updated[0].playableUrl,
hackatimeProject: updated[0].hackatimeProject,
slackId: user.slackId
}).catch(err => console.error('[YSWS] failed:', err))
// Send Slack DM notification that the project is waiting for review
if (config.slackBotToken && user.slackId) {
try {

View file

@ -106,6 +106,7 @@ export default {
sort: 'sort:',
default: 'default',
favorites: 'favorites',
favoritesSortHint: 'most wished, then yours',
probability: 'probability',
cost: 'cost (low to high)',
loadingItems: 'Loading items...',

View file

@ -106,6 +106,7 @@ export default {
sort: 'ordenar:',
default: 'predeterminado',
favorites: 'favoritos',
favoritesSortHint: 'más deseados, luego los tuyos',
probability: 'probabilidad',
cost: 'costo (bajo a alto)',
loadingItems: 'Cargando artículos...',

View file

@ -59,7 +59,15 @@
let sortedItems = $derived.by(() => {
let items = [...filteredItems];
if (sortBy === 'favorites') {
return items.sort((a, b) => b.heartCount - a.heartCount);
return items.sort((a, b) => {
if (b.heartCount !== a.heartCount) {
return b.heartCount - a.heartCount;
}
if (a.userHearted !== b.userHearted) {
return a.userHearted ? -1 : 1;
}
return a.id - b.id;
});
} else if (sortBy === 'probability') {
return items.sort((a, b) => b.effectiveProbability - a.effectiveProbability);
} else if (sortBy === 'cost') {
@ -224,7 +232,7 @@
? 'bg-black text-white'
: 'hover:border-dashed'}"
>
{$t.shop.favorites}
{$t.shop.favorites} ({$t.shop.favoritesSortHint})
</button>
<button
onclick={() => (sortBy = 'probability')}