diff --git a/backend/drizzle/0003_add_project_update_fields.sql b/backend/drizzle/0003_add_project_update_fields.sql new file mode 100644 index 0000000..c3f3e40 --- /dev/null +++ b/backend/drizzle/0003_add_project_update_fields.sql @@ -0,0 +1,2 @@ +ALTER TABLE "projects" ADD COLUMN "is_update" integer DEFAULT 0;--> statement-breakpoint +ALTER TABLE "projects" ADD COLUMN "update_description" text; diff --git a/backend/drizzle/0004_remove_is_update_add_ai_description.sql b/backend/drizzle/0004_remove_is_update_add_ai_description.sql new file mode 100644 index 0000000..6f67594 --- /dev/null +++ b/backend/drizzle/0004_remove_is_update_add_ai_description.sql @@ -0,0 +1,2 @@ +ALTER TABLE "projects" DROP COLUMN IF EXISTS "is_update";--> statement-breakpoint +ALTER TABLE "projects" ADD COLUMN "ai_description" text; diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index fc6b61b..4538412 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1770422400000, "tag": "0002_feedback_and_activity", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1770508800000, + "tag": "0003_add_project_update_fields", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1770595200000, + "tag": "0004_remove_is_update_add_ai_description", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/lib/airtable-sync.ts b/backend/src/lib/airtable-sync.ts index 7271e9a..4b894ff 100644 --- a/backend/src/lib/airtable-sync.ts +++ b/backend/src/lib/airtable-sync.ts @@ -42,6 +42,8 @@ async function syncProjectsToAirtable(): Promise { hoursOverride: projectsTable.hoursOverride, tier: projectsTable.tier, status: projectsTable.status, + updateDescription: projectsTable.updateDescription, + aiDescription: projectsTable.aiDescription, feedbackSource: projectsTable.feedbackSource, feedbackGood: projectsTable.feedbackGood, feedbackImprove: projectsTable.feedbackImprove, @@ -117,9 +119,17 @@ async function syncProjectsToAirtable(): Promise { const firstName = userInfo?.given_name || (project.username || '').split(' ')[0] || '' const lastName = userInfo?.family_name || (project.username || '').split(' ').slice(1).join(' ') || '' + const descriptionParts = [project.description || ''] + if (project.updateDescription) { + descriptionParts.push(`\nThis project is an update. ${project.updateDescription}`) + } + if (project.aiDescription) { + descriptionParts.push(`\nAI was used in this project. ${project.aiDescription}`) + } + const fields: Airtable.FieldSet = { 'Code URL': project.githubUrl, - 'Description': project.description || '', + 'Description': descriptionParts.join('\n'), 'Email': project.email || '', 'First Name': firstName, 'Last Name': lastName, diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index 32b440b..761e56c 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -392,6 +392,9 @@ projects.get('/:id', async ({ params, headers }) => { status: project[0].status, scrapsAwarded: project[0].scrapsAwarded, views: project[0].views, + updateDescription: project[0].updateDescription, + aiDescription: isOwner ? project[0].aiDescription : undefined, + usedAi: !!project[0].aiDescription, createdAt: project[0].createdAt, updatedAt: project[0].updatedAt }, @@ -413,6 +416,8 @@ projects.post('/', async ({ body, headers }) => { githubUrl?: string hackatimeProject?: string tier?: number + updateDescription?: string + aiDescription?: string } if (!validateImageUrl(data.image)) { @@ -437,7 +442,9 @@ projects.post('/', async ({ body, headers }) => { githubUrl: data.githubUrl || null, hackatimeProject: projectName || null, hours: 0, - tier + tier, + updateDescription: data.updateDescription || null, + aiDescription: data.aiDescription || null }) .returning() @@ -482,6 +489,8 @@ projects.put('/:id', async ({ params, body, headers }) => { playableUrl?: string | null hackatimeProject?: string | null tier?: number + updateDescription?: string | null + aiDescription?: string | null } if (!validateImageUrl(data.image)) { @@ -511,6 +520,8 @@ projects.put('/:id', async ({ params, body, headers }) => { playableUrl: data.playableUrl, hackatimeProject: projectName, tier, + updateDescription: data.updateDescription !== undefined ? (data.updateDescription || null) : undefined, + aiDescription: data.aiDescription !== undefined ? (data.aiDescription || null) : undefined, updatedAt: new Date() }) .where(and(eq(projectsTable.id, parseInt(params.id)), eq(projectsTable.userId, user.id))) diff --git a/backend/src/schemas/projects.ts b/backend/src/schemas/projects.ts index 2467f76..34e5c81 100644 --- a/backend/src/schemas/projects.ts +++ b/backend/src/schemas/projects.ts @@ -22,6 +22,12 @@ export const projectsTable = pgTable('projects', { scrapsAwarded: integer('scraps_awarded').notNull().default(0), views: integer().notNull().default(0), + // Update fields + updateDescription: text('update_description'), + + // AI usage fields + aiDescription: text('ai_description'), + // Feedback fields (filled on submission) feedbackSource: text('feedback_source'), feedbackGood: text('feedback_good'), diff --git a/frontend/src/lib/components/CreateProjectModal.svelte b/frontend/src/lib/components/CreateProjectModal.svelte index 13806d1..d2ad7f3 100644 --- a/frontend/src/lib/components/CreateProjectModal.svelte +++ b/frontend/src/lib/components/CreateProjectModal.svelte @@ -60,6 +60,10 @@ let loading = $state(false); let error = $state(null); let selectedTier = $state(1); + let isUpdate = $state(false); + let updateDescription = $state(''); + let usedAi = $state(false); + let aiDescription = $state(''); const TIERS = [ { value: 1, description: $t.createProject.tierDescriptions.tier1 }, @@ -78,7 +82,9 @@ description.trim().length >= DESC_MIN && description.trim().length <= DESC_MAX ); let hasName = $derived(name.trim().length > 0 && name.trim().length <= NAME_MAX); - let allRequirementsMet = $derived(hasDescription && hasName); + let updateValid = $derived(!isUpdate || updateDescription.trim().length > 0); + let aiValid = $derived(!usedAi || aiDescription.trim().length > 0); + let allRequirementsMet = $derived(hasDescription && hasName && updateValid && aiValid); async function fetchHackatimeProjects() { loadingProjects = true; @@ -173,6 +179,10 @@ selectedHackatimeProjects = []; showDropdown = false; selectedTier = 1; + isUpdate = false; + updateDescription = ''; + usedAi = false; + aiDescription = ''; error = null; } @@ -201,7 +211,9 @@ image: imageUrl || null, githubUrl: finalGithubUrl, hackatimeProject: hackatimeValue, - tier: selectedTier + tier: selectedTier, + updateDescription: isUpdate ? updateDescription : null, + aiDescription: usedAi ? aiDescription : null }) }); @@ -465,6 +477,66 @@ + +
+ +
+ + {#if isUpdate} +
+ + + {#if updateDescription.trim().length === 0} +

{$t.createProject.pleaseDescribeUpdate}

+ {/if} +
+ {/if} + + +
+ +
+ + {#if usedAi} +
+ + + {#if aiDescription.trim().length === 0} +

{$t.createProject.pleaseDescribeAiUsage}

+ {/if} +
+ {/if} +

{$t.createProject.requirements}

diff --git a/frontend/src/lib/i18n/en.ts b/frontend/src/lib/i18n/en.ts index 85137eb..4a5c338 100644 --- a/frontend/src/lib/i18n/en.ts +++ b/frontend/src/lib/i18n/en.ts @@ -269,7 +269,15 @@ export default { tier2: 'moderate complexity, multi-file projects', tier3: 'complex features, APIs, integrations', tier4: 'full applications, major undertakings' - } + }, + isUpdateLabel: 'this project is an update to a previous project', + whatDidYouUpdate: 'what did you update?', + updateDescriptionPlaceholder: 'Describe what you changed or improved in this update...', + pleaseDescribeUpdate: 'Please describe what you updated', + usedAiLabel: 'AI was used in this project', + howWasAiUsed: 'how was AI used?', + aiDescriptionPlaceholder: 'Describe how AI was used in this project...', + pleaseDescribeAiUsage: 'Please describe how AI was used' }, address: { shippingAddress: 'shipping address', @@ -374,7 +382,17 @@ export default { unsubmitConfirmTitle: 'unsubmit project?', unsubmitConfirmMessage: 'this will remove your project from the review queue and return it to in-progress status.', unsubmitting: 'unsubmitting...', - unsubmitSuccess: 'project unsubmitted successfully' + unsubmitSuccess: 'project unsubmitted successfully', + isUpdateLabel: 'this project is an update to a previous project', + whatDidYouUpdate: 'what did you update?', + updateDescriptionPlaceholder: 'Describe what you changed or improved in this update...', + pleaseDescribeUpdate: 'Please describe what you updated', + whatWasUpdated: 'what was updated', + usedAiLabel: 'AI was used in this project', + howWasAiUsed: 'how was AI used?', + aiDescriptionPlaceholder: 'Describe how AI was used in this project...', + pleaseDescribeAiUsage: 'Please describe how AI was used', + aiWasUsed: 'AI was used in this project' }, faq: { title: 'faq', diff --git a/frontend/src/lib/i18n/es.ts b/frontend/src/lib/i18n/es.ts index 29ce36b..388924c 100644 --- a/frontend/src/lib/i18n/es.ts +++ b/frontend/src/lib/i18n/es.ts @@ -269,7 +269,15 @@ export default { tier2: 'complejidad moderada, proyectos multi-archivo', tier3: 'características complejas, APIs, integraciones', tier4: 'aplicaciones completas, emprendimientos mayores' - } + }, + isUpdateLabel: 'este proyecto es una actualización de un proyecto anterior', + whatDidYouUpdate: '¿qué actualizaste?', + updateDescriptionPlaceholder: 'Describe qué cambiaste o mejoraste en esta actualización...', + pleaseDescribeUpdate: 'Por favor describe qué actualizaste', + usedAiLabel: 'se usó IA en este proyecto', + howWasAiUsed: '¿cómo se usó la IA?', + aiDescriptionPlaceholder: 'Describe cómo se usó la IA en este proyecto...', + pleaseDescribeAiUsage: 'Por favor describe cómo se usó la IA' }, address: { shippingAddress: 'dirección de envío', @@ -414,7 +422,17 @@ export default { unsubmitConfirmTitle: '¿retirar proyecto?', unsubmitConfirmMessage: 'esto eliminará tu proyecto de la cola de revisión y lo devolverá al estado en progreso.', unsubmitting: 'retirando...', - unsubmitSuccess: 'proyecto retirado exitosamente' + unsubmitSuccess: 'proyecto retirado exitosamente', + isUpdateLabel: 'este proyecto es una actualización de un proyecto anterior', + whatDidYouUpdate: '¿qué actualizaste?', + updateDescriptionPlaceholder: 'Describe qué cambiaste o mejoraste en esta actualización...', + pleaseDescribeUpdate: 'Por favor describe qué actualizaste', + whatWasUpdated: 'qué se actualizó', + usedAiLabel: 'se usó IA en este proyecto', + howWasAiUsed: '¿cómo se usó la IA?', + aiDescriptionPlaceholder: 'Describe cómo se usó la IA en este proyecto...', + pleaseDescribeAiUsage: 'Por favor describe cómo se usó la IA', + aiWasUsed: 'se usó IA en este proyecto' }, profile: { backToLeaderboard: 'volver a clasificación', diff --git a/frontend/src/routes/admin/reviews/[id]/+page.svelte b/frontend/src/routes/admin/reviews/[id]/+page.svelte index 866c09a..e8ccccb 100644 --- a/frontend/src/routes/admin/reviews/[id]/+page.svelte +++ b/frontend/src/routes/admin/reviews/[id]/+page.svelte @@ -12,7 +12,8 @@ XCircle, Info, Globe, - RefreshCw + RefreshCw, + Bot } from '@lucide/svelte'; import ProjectPlaceholder from '$lib/components/ProjectPlaceholder.svelte'; import { getUser } from '$lib/auth-client'; @@ -57,6 +58,8 @@ feedbackSource: string | null; feedbackGood: string | null; feedbackImprove: string | null; + updateDescription: string | null; + aiDescription: string | null; } interface User { @@ -343,6 +346,24 @@

{project.description}

+ {#if project.updateDescription} +
+

+ + {$t.project.whatWasUpdated} +

+

{project.updateDescription}

+
+ {/if} + {#if project.aiDescription} +
+

+ + {$t.project.aiWasUsed} +

+

{project.aiDescription}

+
+ {/if}
{formatHours(project.hours)}h logged {/if}
-
+
{$t.project.tier.replace('{value}', String(project.tier))} + {#if project.usedAi} + + + {$t.project.aiWasUsed} + + {/if}
{#if project.description} @@ -292,6 +303,16 @@

{$t.project.noDescriptionYet}

{/if} + {#if project.updateDescription} +
+

+ + {$t.project.whatWasUpdated} +

+

{project.updateDescription}

+
+ {/if} +
0); + let aiValid = $derived(!usedAi || aiDescription.trim().length > 0); + let canSave = $derived(hasDescription && hasName && githubValidation.valid && playableValidation.valid && updateValid && aiValid); onMount(async () => { const user = await getUser(); @@ -89,6 +97,10 @@ project = responseData.project; imagePreview = project?.image || null; selectedTier = project?.tier || 1; + isUpdate = !!(project?.updateDescription); + updateDescription = project?.updateDescription || ''; + usedAi = !!(project?.aiDescription); + aiDescription = project?.aiDescription || ''; if (project?.hackatimeProject) { selectedHackatimeNames = project.hackatimeProject.split(',').map((p: string) => p.trim()).filter((p: string) => p.length > 0); } @@ -216,7 +228,9 @@ githubUrl: project.githubUrl, playableUrl: project.playableUrl, hackatimeProject: hackatimeValue, - tier: selectedTier + tier: selectedTier, + updateDescription: isUpdate ? updateDescription : null, + aiDescription: usedAi ? aiDescription : null }) }); @@ -505,6 +519,66 @@ {/each}
+ + +
+ +
+ + {#if isUpdate} +
+ + + {#if updateDescription.trim().length === 0} +

{$t.project.pleaseDescribeUpdate}

+ {/if} +
+ {/if} + + +
+ +
+ + {#if usedAi} +
+ + + {#if aiDescription.trim().length === 0} +

{$t.project.pleaseDescribeAiUsage}

+ {/if} +
+ {/if} diff --git a/frontend/src/routes/projects/[id]/view/+page.svelte b/frontend/src/routes/projects/[id]/view/+page.svelte index da3acd7..95a0ac1 100644 --- a/frontend/src/routes/projects/[id]/view/+page.svelte +++ b/frontend/src/routes/projects/[id]/view/+page.svelte @@ -7,7 +7,9 @@ Clock, CheckCircle, AlertTriangle, - Package + Package, + RefreshCw, + Bot } from '@lucide/svelte'; import { API_URL } from '$lib/config'; import { formatHours } from '$lib/utils'; @@ -26,6 +28,8 @@ playableUrl: string | null; hours: number; status: string; + updateDescription: string | null; + usedAi: boolean; createdAt: string; } @@ -131,6 +135,25 @@ {/if}

{project.description}

+ {#if project.updateDescription} +
+

+ + {$t.project.whatWasUpdated} +

+

{project.updateDescription}

+
+ {/if} +
+ {#if project.usedAi} + + + {$t.project.aiWasUsed} + + {/if} +