Skip to content

Commit cf9140d

Browse files
committed
feat: Initialize Rasoi Planner application
This commit sets up the foundational files for the Rasoi Planner application, including: - README for project description and local setup instructions. - .env.example for environment variable configuration. - .gitignore for specifying ignored files. - index.html as the main HTML entry point. - metadata.json for app metadata. - package.json with project dependencies and scripts. - Initial structure for App.tsx, index.css, main.tsx, and tsconfig.json. - vite.config.ts for Vite build configurations. - src/types.ts for defining application data structures. - src/components/OwnerView.tsx with basic owner-specific UI components. - src/firebase.ts for Firebase integration setup.
1 parent e8eb6b5 commit cf9140d

25 files changed

Lines changed: 7215 additions & 5 deletions

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# GEMINI_API_KEY: Required for Gemini AI API calls.
2+
# AI Studio automatically injects this at runtime from user secrets.
3+
# Users configure this via the Secrets panel in the AI Studio UI.
4+
GEMINI_API_KEY="MY_GEMINI_API_KEY"
5+
6+
# APP_URL: The URL where this applet is hosted.
7+
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
8+
# Used for self-referential links, OAuth callbacks, and API endpoints.
9+
APP_URL="MY_APP_URL"

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules/
2+
build/
3+
dist/
4+
coverage/
5+
.DS_Store
6+
*.log
7+
.env*
8+
!.env.example

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
<div align="center">
2-
32
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3+
</div>
44

5-
<h1>Built with AI Studio</h2>
5+
# Run and deploy your AI Studio app
66

7-
<p>The fastest path from prompt to production with Gemini.</p>
7+
This contains everything you need to run your app locally.
88

9-
<a href="https://aistudio.google.com/apps">Start building</a>
9+
View your app in AI Studio: https://ai.studio/apps/3900af62-0bf5-496a-a136-d1c8a0c4b8bd
1010

11-
</div>
11+
## Run Locally
12+
13+
**Prerequisites:** Node.js
14+
15+
16+
1. Install dependencies:
17+
`npm install`
18+
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19+
3. Run the app:
20+
`npm run dev`

firebase-applet-config.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"projectId": "gen-lang-client-0862152879",
3+
"appId": "1:161052050586:web:7103288554814434e40b23",
4+
"apiKey": "AIzaSyBsnsmsstRPVd79zowTwwrK4HxnAnRQsQw",
5+
"authDomain": "gen-lang-client-0862152879.firebaseapp.com",
6+
"firestoreDatabaseId": "ai-studio-3900af62-0bf5-496a-a136-d1c8a0c4b8bd",
7+
"storageBucket": "gen-lang-client-0862152879.firebasestorage.app",
8+
"messagingSenderId": "161052050586",
9+
"measurementId": ""
10+
}

firebase-blueprint.json

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"entities": {
3+
"Household": {
4+
"title": "Household",
5+
"description": "Represents a household with an owner and an optional cook.",
6+
"type": "object",
7+
"properties": {
8+
"ownerId": { "type": "string" },
9+
"cookEmail": { "type": "string" }
10+
},
11+
"required": ["ownerId"]
12+
},
13+
"InventoryItem": {
14+
"title": "Inventory Item",
15+
"description": "An item in the pantry",
16+
"type": "object",
17+
"properties": {
18+
"name": { "type": "string" },
19+
"nameHi": { "type": "string" },
20+
"category": { "type": "string" },
21+
"status": { "type": "string", "enum": ["in-stock", "low", "out"] },
22+
"icon": { "type": "string" },
23+
"lastUpdated": { "type": "string" },
24+
"updatedBy": { "type": "string", "enum": ["owner", "cook"] },
25+
"verificationNeeded": { "type": "boolean" },
26+
"anomalyReason": { "type": "string" },
27+
"defaultQuantity": { "type": "string" },
28+
"requestedQuantity": { "type": "string" }
29+
},
30+
"required": ["name", "category", "status", "icon"]
31+
},
32+
"MealPlan": {
33+
"title": "Meal Plan",
34+
"description": "Daily meal plan",
35+
"type": "object",
36+
"properties": {
37+
"morning": { "type": "string" },
38+
"evening": { "type": "string" },
39+
"notes": { "type": "string" },
40+
"leftovers": { "type": "string" }
41+
}
42+
},
43+
"PantryLog": {
44+
"title": "Pantry Log",
45+
"description": "Log of inventory changes",
46+
"type": "object",
47+
"properties": {
48+
"itemId": { "type": "string" },
49+
"itemName": { "type": "string" },
50+
"oldStatus": { "type": "string" },
51+
"newStatus": { "type": "string" },
52+
"timestamp": { "type": "string" },
53+
"role": { "type": "string", "enum": ["owner", "cook"] }
54+
},
55+
"required": ["itemId", "itemName", "oldStatus", "newStatus", "timestamp", "role"]
56+
}
57+
},
58+
"firestore": {
59+
"/households/{householdId}": {
60+
"schema": { "$ref": "#/entities/Household" },
61+
"description": "Household documents containing owner and cook info"
62+
},
63+
"/households/{householdId}/inventory/{itemId}": {
64+
"schema": { "$ref": "#/entities/InventoryItem" },
65+
"description": "Household's inventory items"
66+
},
67+
"/households/{householdId}/meals/{date}": {
68+
"schema": { "$ref": "#/entities/MealPlan" },
69+
"description": "Household's meal plans by date"
70+
},
71+
"/households/{householdId}/logs/{logId}": {
72+
"schema": { "$ref": "#/entities/PantryLog" },
73+
"description": "Household's pantry activity logs"
74+
}
75+
}
76+
}

firestore.rules

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
rules_version = '2';
2+
service cloud.firestore {
3+
match /databases/{database}/documents {
4+
5+
// ===============================================================
6+
// Assumed Data Model
7+
// ===============================================================
8+
// Collection: households/{householdId}
9+
// Document ID: auto-generated or owner's uid
10+
// Fields:
11+
// - ownerId: string (required)
12+
// - cookEmail: string (optional)
13+
//
14+
// Collection: households/{householdId}/inventory
15+
// Document ID: auto-generated or custom string
16+
// Fields:
17+
// - name: string (required, max 100 chars)
18+
// - nameHi: string (optional, max 100 chars)
19+
// - category: string (required, max 50 chars)
20+
// - status: string (required, 'in-stock', 'low', 'out')
21+
// - icon: string (required, max 10 chars)
22+
// - lastUpdated: string (optional, ISO date)
23+
// - updatedBy: string (optional, 'owner', 'cook')
24+
// - verificationNeeded: boolean (optional)
25+
// - anomalyReason: string (optional, max 500 chars)
26+
// - defaultQuantity: string (optional, max 50 chars)
27+
// - requestedQuantity: string (optional, max 50 chars)
28+
//
29+
// Collection: households/{householdId}/meals
30+
// Document ID: YYYY-MM-DD string
31+
// Fields:
32+
// - morning: string (optional, max 1000 chars)
33+
// - evening: string (optional, max 1000 chars)
34+
// - notes: string (optional, max 1000 chars)
35+
// - leftovers: string (optional, max 1000 chars)
36+
//
37+
// Collection: households/{householdId}/logs
38+
// Document ID: auto-generated
39+
// Fields:
40+
// - itemId: string (required, max 100 chars)
41+
// - itemName: string (required, max 100 chars)
42+
// - oldStatus: string (required, 'in-stock', 'low', 'out', '')
43+
// - newStatus: string (required, 'in-stock', 'low', 'out')
44+
// - timestamp: string (required, ISO date)
45+
// - role: string (required, 'owner', 'cook')
46+
// ===============================================================
47+
48+
function isAuthenticated() {
49+
return request.auth != null;
50+
}
51+
52+
function isOwner(householdId) {
53+
return isAuthenticated() &&
54+
get(/databases/$(database)/documents/households/$(householdId)).data.ownerId == request.auth.uid;
55+
}
56+
57+
function isCook(householdId) {
58+
return isAuthenticated() &&
59+
get(/databases/$(database)/documents/households/$(householdId)).data.cookEmail == request.auth.token.email;
60+
}
61+
62+
function isHouseholdMember(householdId) {
63+
return isOwner(householdId) || isCook(householdId);
64+
}
65+
66+
function isValidString(val, minLen, maxLen) {
67+
return val is string && val.size() >= minLen && val.size() <= maxLen;
68+
}
69+
70+
function isValidOptionalString(val, minLen, maxLen) {
71+
return val == null || (val is string && val.size() >= minLen && val.size() <= maxLen);
72+
}
73+
74+
function isValidInventoryItem(data) {
75+
return data.keys().hasAll(['name', 'category', 'status', 'icon']) &&
76+
isValidString(data.name, 1, 100) &&
77+
isValidString(data.category, 1, 50) &&
78+
data.status in ['in-stock', 'low', 'out'] &&
79+
isValidString(data.icon, 1, 10) &&
80+
(!('nameHi' in data) || isValidOptionalString(data.nameHi, 0, 100)) &&
81+
(!('lastUpdated' in data) || data.lastUpdated is string) &&
82+
(!('updatedBy' in data) || data.updatedBy in ['owner', 'cook']) &&
83+
(!('verificationNeeded' in data) || data.verificationNeeded is bool) &&
84+
(!('anomalyReason' in data) || isValidOptionalString(data.anomalyReason, 0, 500)) &&
85+
(!('defaultQuantity' in data) || isValidOptionalString(data.defaultQuantity, 0, 50)) &&
86+
(!('requestedQuantity' in data) || isValidOptionalString(data.requestedQuantity, 0, 50));
87+
}
88+
89+
function isValidMealPlan(data) {
90+
return (!('morning' in data) || isValidOptionalString(data.morning, 0, 1000)) &&
91+
(!('evening' in data) || isValidOptionalString(data.evening, 0, 1000)) &&
92+
(!('notes' in data) || isValidOptionalString(data.notes, 0, 1000)) &&
93+
(!('leftovers' in data) || isValidOptionalString(data.leftovers, 0, 1000));
94+
}
95+
96+
function isValidPantryLog(data) {
97+
return data.keys().hasAll(['itemId', 'itemName', 'oldStatus', 'newStatus', 'timestamp', 'role']) &&
98+
isValidString(data.itemId, 1, 100) &&
99+
isValidString(data.itemName, 1, 100) &&
100+
data.oldStatus in ['in-stock', 'low', 'out', ''] &&
101+
data.newStatus in ['in-stock', 'low', 'out'] &&
102+
data.timestamp is string &&
103+
data.role in ['owner', 'cook'];
104+
}
105+
106+
match /households/{householdId} {
107+
allow read: if isAuthenticated() && (
108+
resource.data.ownerId == request.auth.uid ||
109+
resource.data.cookEmail == request.auth.token.email
110+
);
111+
112+
allow create: if isAuthenticated() &&
113+
request.resource.data.ownerId == request.auth.uid;
114+
115+
allow update: if isAuthenticated() &&
116+
resource.data.ownerId == request.auth.uid &&
117+
request.resource.data.ownerId == resource.data.ownerId;
118+
119+
allow delete: if isAuthenticated() && resource.data.ownerId == request.auth.uid;
120+
121+
match /inventory/{itemId} {
122+
allow read: if isHouseholdMember(householdId);
123+
allow create: if isHouseholdMember(householdId) && isValidInventoryItem(request.resource.data);
124+
allow update: if isHouseholdMember(householdId) && isValidInventoryItem(request.resource.data);
125+
allow delete: if isOwner(householdId);
126+
}
127+
128+
match /meals/{date} {
129+
allow read: if isHouseholdMember(householdId);
130+
allow create: if isOwner(householdId) && isValidMealPlan(request.resource.data);
131+
allow update: if isOwner(householdId) && isValidMealPlan(request.resource.data);
132+
allow delete: if isOwner(householdId);
133+
}
134+
135+
match /logs/{logId} {
136+
allow read: if isHouseholdMember(householdId);
137+
allow create: if isHouseholdMember(householdId) && isValidPantryLog(request.resource.data);
138+
allow update: if false; // Logs are immutable
139+
allow delete: if isOwner(householdId);
140+
}
141+
}
142+
143+
// Keep legacy users path for migration
144+
match /users/{userId} {
145+
allow read, write: if isAuthenticated() && request.auth.uid == userId;
146+
match /{document=**} {
147+
allow read, write: if isAuthenticated() && request.auth.uid == userId;
148+
}
149+
}
150+
}
151+
}

index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>My Google AI Studio App</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
13+

metadata.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Rasoi Planner",
3+
"description": "A meal and pantry planner for Indian households, connecting owners and cooks.",
4+
"requestFramePermissions": []
5+
}

0 commit comments

Comments
 (0)