Grabby-Voice-Classic / src /lib /server /userReviewStore.ts
moonlantern1's picture
Build Foodstar user journal flow
d99b1af
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import path from 'path';
export type SavedFoodReview = {
id: string;
userEmail: string;
submissionId: string;
campaignSlug: string;
placeName: string;
dishName: string;
privacy: 'private' | 'public';
createdAt: number;
};
const LOCAL_DATA_DIR = path.join(process.cwd(), '.local-review-data');
const USER_REVIEWS_FILE = path.join(LOCAL_DATA_DIR, 'user-reviews.json');
function ensureLocalDir() {
mkdirSync(LOCAL_DATA_DIR, { recursive: true });
}
function readAllReviews(): SavedFoodReview[] {
if (!existsSync(USER_REVIEWS_FILE)) return [];
try {
const parsed = JSON.parse(readFileSync(USER_REVIEWS_FILE, 'utf8')) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.filter(isSavedReview);
} catch {
return [];
}
}
function writeAllReviews(reviews: SavedFoodReview[]) {
ensureLocalDir();
writeFileSync(USER_REVIEWS_FILE, `${JSON.stringify(reviews, null, 2)}\n`, 'utf8');
}
function isSavedReview(value: unknown): value is SavedFoodReview {
const maybe = value as Partial<SavedFoodReview>;
return (
typeof maybe.id === 'string' &&
typeof maybe.userEmail === 'string' &&
typeof maybe.submissionId === 'string' &&
typeof maybe.campaignSlug === 'string' &&
typeof maybe.placeName === 'string' &&
typeof maybe.dishName === 'string' &&
(maybe.privacy === 'private' || maybe.privacy === 'public') &&
typeof maybe.createdAt === 'number'
);
}
function reviewId(userEmail: string, submissionId: string) {
return `${userEmail.toLowerCase()}:${submissionId}`;
}
export function saveReviewForUser(input: {
userEmail: string;
submissionId: string;
campaignSlug: string;
placeName?: string | null;
dishName?: string | null;
}) {
const userEmail = input.userEmail.trim().toLowerCase();
if (!userEmail || !input.submissionId) return null;
const reviews = readAllReviews();
const id = reviewId(userEmail, input.submissionId);
const existingIndex = reviews.findIndex((review) => review.id === id);
const review: SavedFoodReview = {
id,
userEmail,
submissionId: input.submissionId,
campaignSlug: input.campaignSlug,
placeName: input.placeName?.trim() || 'Foodstar review',
dishName: input.dishName?.trim() || 'Micro food review',
privacy: 'private',
createdAt: existingIndex >= 0 ? reviews[existingIndex]!.createdAt : Date.now(),
};
if (existingIndex >= 0) reviews[existingIndex] = review;
else reviews.push(review);
writeAllReviews(reviews);
return review;
}
export function listReviewsForUser(userEmail: string) {
const normalized = userEmail.trim().toLowerCase();
return readAllReviews()
.filter((review) => review.userEmail === normalized)
.sort((a, b) => b.createdAt - a.createdAt);
}