mirror of
https://github.com/andwati/agentathon.git
synced 2026-10-18 06:02:53 +00:00
feat: Initialize Team Project Tracker app
Sets up the basic structure for the Team Project Tracker application. This includes: - Initializing React components for project management. - Configuring Vite for development and build processes. - Defining project metadata and environment variables. - Providing instructions for local development.
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||||
|
# AI Studio automatically injects this at runtime from user secrets.
|
||||||
|
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||||
|
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||||
|
|
||||||
|
# APP_URL: The URL where this applet is hosted.
|
||||||
|
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||||
|
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||||
|
APP_URL="MY_APP_URL"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||||
|
|
||||||
<h1>Built with AI Studio</h2>
|
|
||||||
|
|
||||||
<p>The fastest path from prompt to production with Gemini.</p>
|
|
||||||
|
|
||||||
<a href="https://aistudio.google.com/apps">Start building</a>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
# Run and deploy your AI Studio app
|
||||||
|
|
||||||
|
This contains everything you need to run your app locally.
|
||||||
|
|
||||||
|
View your app in AI Studio: https://ai.studio/apps/a086d74b-a8b6-4b42-ae0d-08271ccd7b66
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
**Prerequisites:** Node.js
|
||||||
|
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
`npm install`
|
||||||
|
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||||
|
3. Run the app:
|
||||||
|
`npm run dev`
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>My Google AI Studio App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Team Project Tracker",
|
||||||
|
"description": "A streamlined collaborative tool for tracking team projects, owners, and real-time status updates.",
|
||||||
|
"requestFramePermissions": [],
|
||||||
|
"majorCapabilities": [
|
||||||
|
"MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "react-example",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"clean": "rm -rf dist server.js",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google/genai": "^1.29.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
|
"react": "^19.0.1",
|
||||||
|
"react-dom": "^19.0.1",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"motion": "^12.23.24"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"@types/express": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, FormEvent } from 'react';
|
||||||
|
import { Plus, Trash2, LayoutGrid, CheckCircle2, Circle, Clock } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
|
||||||
|
type ProjectStatus = 'Not Started' | 'In Progress' | 'Done';
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
status: ProjectStatus;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [projects, setProjects] = useState<Project[]>(() => {
|
||||||
|
const saved = localStorage.getItem('team-projects');
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
owner: '',
|
||||||
|
status: 'Not Started' as ProjectStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('team-projects', JSON.stringify(projects));
|
||||||
|
}, [projects]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!formData.name || !formData.owner) return;
|
||||||
|
|
||||||
|
const newProject: Project = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
...formData,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setProjects([newProject, ...projects]);
|
||||||
|
setFormData({ name: '', owner: '', status: 'Not Started' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProject = (id: string) => {
|
||||||
|
setProjects(projects.filter(p => p.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
'Not Started': 'text-red-500',
|
||||||
|
'In Progress': 'text-amber-500',
|
||||||
|
'Done': 'text-emerald-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusIcons = {
|
||||||
|
'Not Started': <Circle className="w-4 h-4" />,
|
||||||
|
'In Progress': <Clock className="w-4 h-4" />,
|
||||||
|
'Done': <CheckCircle2 className="w-4 h-4" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-neutral-50 text-neutral-900 font-sans selection:bg-blue-100 p-4 md:p-8">
|
||||||
|
<div className="max-w-5xl mx-auto space-y-8">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<header className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-blue-600 mb-1">
|
||||||
|
<LayoutGrid className="w-5 h-5" />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-widest">Workspace</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight text-neutral-900">Team Project Tracker</h1>
|
||||||
|
<p className="text-neutral-500 mt-2 max-w-md">
|
||||||
|
Manage your team's workload with a simple, high-visibility tracking system.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="bg-white border border-neutral-200 px-4 py-2 rounded-lg shadow-sm">
|
||||||
|
<span className="text-xs text-neutral-400 block uppercase font-bold tracking-tighter">Total Projects</span>
|
||||||
|
<span className="text-xl font-bold">{projects.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white border border-neutral-200 px-4 py-2 rounded-lg shadow-sm">
|
||||||
|
<span className="text-xs text-neutral-400 block uppercase font-bold tracking-tighter">Completion</span>
|
||||||
|
<span className="text-xl font-bold">
|
||||||
|
{projects.length > 0 ? Math.round((projects.filter(p => p.status === 'Done').length / projects.length) * 100) : 0}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Input Form Section */}
|
||||||
|
<motion.section
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl border border-neutral-200 shadow-sm p-6"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-6 items-end">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold uppercase text-neutral-400 tracking-wider">Project Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enterprise Migration..."
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
className="w-full bg-white border border-neutral-200 rounded-xl px-4 py-3 text-neutral-600 placeholder:text-neutral-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold uppercase text-neutral-400 tracking-wider">Owner</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Sarah Mitchell"
|
||||||
|
value={formData.owner}
|
||||||
|
onChange={(e) => setFormData({ ...formData, owner: e.target.value })}
|
||||||
|
className="w-full bg-white border border-neutral-200 rounded-xl px-4 py-3 text-neutral-600 placeholder:text-neutral-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold uppercase text-neutral-400 tracking-wider">Status</label>
|
||||||
|
<select
|
||||||
|
value={formData.status}
|
||||||
|
onChange={(e) => setFormData({ ...formData, status: e.target.value as ProjectStatus })}
|
||||||
|
className="w-full bg-white border border-neutral-200 rounded-xl px-4 py-3 text-neutral-600 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all font-medium appearance-none cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="Not Started">Not Started</option>
|
||||||
|
<option value="In Progress">In Progress</option>
|
||||||
|
<option value="Done">Done</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-xl transition-all flex items-center justify-center gap-2 shadow-lg shadow-blue-500/20"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
<span>Add Project</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</motion.section>
|
||||||
|
|
||||||
|
{/* Projects List Table */}
|
||||||
|
<section className="bg-white rounded-2xl border border-neutral-200 shadow-sm overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-neutral-100 bg-neutral-50/50">
|
||||||
|
<th className="py-4 px-6 text-xs font-bold uppercase text-neutral-400 tracking-wider w-1/2">Project Detail</th>
|
||||||
|
<th className="py-4 px-6 text-xs font-bold uppercase text-neutral-400 tracking-wider">Team Member</th>
|
||||||
|
<th className="py-4 px-6 text-xs font-bold uppercase text-neutral-400 tracking-wider">Status Update</th>
|
||||||
|
<th className="py-4 px-6 text-xs font-bold uppercase text-neutral-400 tracking-wider text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-neutral-100">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{projects.length > 0 ? (
|
||||||
|
projects.map((project) => (
|
||||||
|
<motion.tr
|
||||||
|
key={project.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 20, scale: 0.95 }}
|
||||||
|
className="group hover:bg-neutral-50 transition-colors"
|
||||||
|
>
|
||||||
|
<td className="py-5 px-6">
|
||||||
|
<span className="text-neutral-900 font-semibold text-lg block">{project.name}</span>
|
||||||
|
<span className="text-neutral-400 text-xs font-medium">Created {new Date(project.createdAt).toLocaleDateString()}</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-xs">
|
||||||
|
{project.owner.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<span className="text-neutral-600 font-medium">{project.owner}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-6">
|
||||||
|
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider ${statusColors[project.status]} bg-white border border-neutral-100 shadow-sm`}>
|
||||||
|
{statusIcons[project.status]}
|
||||||
|
{project.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-6 text-right">
|
||||||
|
<button
|
||||||
|
onClick={() => deleteProject(project.id)}
|
||||||
|
className="p-2 text-neutral-300 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all"
|
||||||
|
title="Delete Project"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</motion.tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="py-20 text-center">
|
||||||
|
<div className="flex flex-col items-center gap-4 text-neutral-300">
|
||||||
|
<div className="p-4 bg-neutral-50 rounded-full">
|
||||||
|
<LayoutGrid className="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium italic">No projects found. Use the form above to get started.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer Info */}
|
||||||
|
<footer className="flex justify-between items-center text-xs text-neutral-400 font-medium pb-8 border-t border-neutral-200 pt-8">
|
||||||
|
<p>© 2026 Team Project Tracker. Precision engineering for teams.</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<span className="hover:text-neutral-900 cursor-pointer transition-colors">Documentation</span>
|
||||||
|
<span className="hover:text-neutral-900 cursor-pointer transition-colors">Support</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {StrictMode} from 'react';
|
||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import {defineConfig, loadEnv} from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig(({mode}) => {
|
||||||
|
const env = loadEnv(mode, '.', '');
|
||||||
|
return {
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
define: {
|
||||||
|
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||||
|
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||||
|
hmr: process.env.DISABLE_HMR !== 'true',
|
||||||
|
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||||
|
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user