Skip to content

Commit a7be94c

Browse files
committed
Merge branch 'main' into azlyth/fly
* main: Feature flags (#20) Wire up search provider/component (#17)
2 parents 5529bc3 + a98ec14 commit a7be94c

File tree

17 files changed

+480
-14
lines changed

17 files changed

+480
-14
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/// <reference path="../pb_data/types.d.ts" />
2+
migrate((app) => {
3+
const collection = new Collection({
4+
"createRule": null,
5+
"deleteRule": null,
6+
"fields": [
7+
{
8+
"autogeneratePattern": "[a-z0-9]{15}",
9+
"hidden": false,
10+
"id": "text3208210256",
11+
"max": 15,
12+
"min": 15,
13+
"name": "id",
14+
"pattern": "^[a-z0-9]+$",
15+
"presentable": false,
16+
"primaryKey": true,
17+
"required": true,
18+
"system": true,
19+
"type": "text"
20+
},
21+
{
22+
"hidden": false,
23+
"id": "bool4087400498",
24+
"name": "enable",
25+
"presentable": false,
26+
"required": false,
27+
"system": false,
28+
"type": "bool"
29+
},
30+
{
31+
"autogeneratePattern": "",
32+
"hidden": false,
33+
"id": "text1579384326",
34+
"max": 0,
35+
"min": 0,
36+
"name": "name",
37+
"pattern": "",
38+
"presentable": false,
39+
"primaryKey": false,
40+
"required": false,
41+
"system": false,
42+
"type": "text"
43+
},
44+
{
45+
"hidden": false,
46+
"id": "autodate2990389176",
47+
"name": "created",
48+
"onCreate": true,
49+
"onUpdate": false,
50+
"presentable": false,
51+
"system": false,
52+
"type": "autodate"
53+
},
54+
{
55+
"hidden": false,
56+
"id": "autodate3332085495",
57+
"name": "updated",
58+
"onCreate": true,
59+
"onUpdate": true,
60+
"presentable": false,
61+
"system": false,
62+
"type": "autodate"
63+
}
64+
],
65+
"id": "pbc_728725186",
66+
"indexes": [],
67+
"listRule": null,
68+
"name": "feature_flags",
69+
"system": false,
70+
"type": "base",
71+
"updateRule": null,
72+
"viewRule": null
73+
});
74+
75+
return app.save(collection);
76+
}, (app) => {
77+
const collection = app.findCollectionByNameOrId("pbc_728725186");
78+
79+
return app.delete(collection);
80+
})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/// <reference path="../pb_data/types.d.ts" />
2+
migrate((app) => {
3+
const collection = app.findCollectionByNameOrId("pbc_728725186")
4+
5+
// add field
6+
collection.fields.addAt(3, new Field({
7+
"autogeneratePattern": "",
8+
"hidden": false,
9+
"id": "text1843675174",
10+
"max": 0,
11+
"min": 0,
12+
"name": "description",
13+
"pattern": "",
14+
"presentable": false,
15+
"primaryKey": false,
16+
"required": false,
17+
"system": false,
18+
"type": "text"
19+
}))
20+
21+
// update field
22+
collection.fields.addAt(1, new Field({
23+
"hidden": false,
24+
"id": "bool4087400498",
25+
"name": "enabled",
26+
"presentable": false,
27+
"required": false,
28+
"system": false,
29+
"type": "bool"
30+
}))
31+
32+
return app.save(collection)
33+
}, (app) => {
34+
const collection = app.findCollectionByNameOrId("pbc_728725186")
35+
36+
// remove field
37+
collection.fields.removeById("text1843675174")
38+
39+
// update field
40+
collection.fields.addAt(1, new Field({
41+
"hidden": false,
42+
"id": "bool4087400498",
43+
"name": "enable",
44+
"presentable": false,
45+
"required": false,
46+
"system": false,
47+
"type": "bool"
48+
}))
49+
50+
return app.save(collection)
51+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="../pb_data/types.d.ts" />
2+
migrate((app) => {
3+
const collection = app.findCollectionByNameOrId("pbc_728725186")
4+
5+
// update collection data
6+
unmarshal({
7+
"listRule": ""
8+
}, collection)
9+
10+
return app.save(collection)
11+
}, (app) => {
12+
const collection = app.findCollectionByNameOrId("pbc_728725186")
13+
14+
// update collection data
15+
unmarshal({
16+
"listRule": null
17+
}, collection)
18+
19+
return app.save(collection)
20+
})

frontend/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@headlessui/react": "^2.2.0",
1313
"@heroicons/react": "^2.2.0",
1414
"next": "15.1.5",
15+
"pocketbase": "^0.25.1",
1516
"react": "^19.0.0",
1617
"react-dom": "^19.0.0",
1718
"react-hot-toast": "^2.5.1",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use client"
2+
3+
import React, { createContext, useContext, ReactNode, useEffect, useState } from 'react';
4+
import { FeatureFlag, loadFeatureFlags, defaultFeatureFlags } from '@/pocketbase/featureFlags';
5+
6+
const FeatureFlagsContext = createContext<FeatureFlag | undefined>(undefined);
7+
8+
interface FeatureFlagsProviderProps {
9+
children: ReactNode;
10+
initialFlags?: FeatureFlag;
11+
}
12+
13+
export const FeatureFlagsProvider: React.FC<FeatureFlagsProviderProps> = ({
14+
children,
15+
initialFlags,
16+
}) => {
17+
const [flags, setFlags] = useState<FeatureFlag>(initialFlags || defaultFeatureFlags);
18+
19+
useEffect(() => {
20+
if (!initialFlags) {
21+
const loadFlags = async () => {
22+
try {
23+
const loadedFlags = await loadFeatureFlags();
24+
setFlags(loadedFlags);
25+
} catch (error) {
26+
console.error('Failed to load feature flags:', error);
27+
}
28+
};
29+
30+
loadFlags();
31+
}
32+
}, [initialFlags]);
33+
34+
return (
35+
<FeatureFlagsContext.Provider value={flags}>
36+
{children}
37+
</FeatureFlagsContext.Provider>
38+
);
39+
};
40+
41+
// Utility hook to check a specific feature flag
42+
export const useFeatureFlag = (flagName: keyof FeatureFlag): boolean => {
43+
const context = useContext(FeatureFlagsContext);
44+
if (context === undefined) {
45+
throw new Error('useFeatureFlags must be used within a FeatureFlagsProvider');
46+
}
47+
return context[flagName];
48+
};

frontend/src/app/contexts/search.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import { createContext, useContext, ReactNode, useState } from 'react';
4+
5+
interface SearchItemAttributes {
6+
bean_type: string;
7+
origin: string;
8+
packaging_type: string;
9+
processing_method: string;
10+
roast_level: string;
11+
variety: string;
12+
}
13+
14+
export interface SearchItem {
15+
id: number;
16+
name: string;
17+
description: string;
18+
price: number;
19+
quantity: number;
20+
rating: number;
21+
imageUrl: string;
22+
attributes: SearchItemAttributes;
23+
tags: string[];
24+
}
25+
26+
interface SearchContextType {
27+
searchItems: SearchItem[];
28+
setSearchItems: (items: SearchItem[]) => void;
29+
selectedItem: SearchItem | null;
30+
setSelectedItem: (item: SearchItem | null) => void;
31+
isLoading: boolean;
32+
setIsLoading: (loading: boolean) => void;
33+
}
34+
35+
const SearchContext = createContext<SearchContextType | undefined>(undefined);
36+
37+
export function SearchProvider({ children }: { children: ReactNode }) {
38+
const [searchItems, setSearchItems] = useState<SearchItem[]>([]);
39+
const [selectedItem, setSelectedItem] = useState<SearchItem | null>(null);
40+
const [isLoading, setIsLoading] = useState(false);
41+
42+
return (
43+
<SearchContext.Provider
44+
value={{
45+
searchItems,
46+
setSearchItems,
47+
selectedItem,
48+
setSelectedItem,
49+
isLoading,
50+
setIsLoading,
51+
}}
52+
>
53+
{children}
54+
</SearchContext.Provider>
55+
);
56+
}
57+
58+
export function useSearch() {
59+
const context = useContext(SearchContext);
60+
if (context === undefined) {
61+
throw new Error('useSearch must be used within a SearchProvider');
62+
}
63+
return context;
64+
}

frontend/src/app/layout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Inter, DM_Sans } from "next/font/google";
33
import "./globals.css";
44
import { AuthProvider } from './contexts/auth';
55
import { CartProvider } from './contexts/cart';
6+
import { FeatureFlagsProvider } from './contexts/featureFlags';
67
import { Toaster } from 'react-hot-toast';
78
import Header from '@/components/Header';
89

@@ -35,12 +36,14 @@ export default function RootLayout({
3536
<body
3637
className={`${inter.variable} ${dmSans.variable} font-sans antialiased`}
3738
>
38-
<AuthProvider>
39-
<CartProvider>
40-
<Header />
41-
{children}
42-
</CartProvider>
43-
</AuthProvider>
39+
<FeatureFlagsProvider>
40+
<AuthProvider>
41+
<CartProvider>
42+
<Header />
43+
{children}
44+
</CartProvider>
45+
</AuthProvider>
46+
</FeatureFlagsProvider>
4447
<Toaster position="bottom-right" />
4548
</body>
4649
</html>

frontend/src/app/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use client';
22

33
import { useState, useEffect } from 'react';
4-
import { useAuth } from './contexts/auth';
54
import Link from 'next/link';
65
import { BuildingStorefrontIcon } from '@heroicons/react/24/outline';
6+
import { SearchProvider } from '@/app/contexts/search';
7+
import { Search } from '@/components/Search';
8+
import { useFeatureFlag } from '@/app/contexts/featureFlags';
79

810
interface Store {
911
id: string;
@@ -19,6 +21,7 @@ export default function Page() {
1921
const [stores, setStores] = useState<Store[]>([]);
2022
const [loading, setLoading] = useState(true);
2123
const [error, setError] = useState<string | null>(null);
24+
const productSearchEnabled = useFeatureFlag('product_search');
2225

2326
useEffect(() => {
2427
const fetchStores = async () => {
@@ -83,6 +86,9 @@ export default function Page() {
8386
<p className="text-lg text-[#4A5568]">
8487
Shop from your favorite local stores with same-day delivery
8588
</p>
89+
<SearchProvider>
90+
{ productSearchEnabled && <Search />}
91+
</SearchProvider>
8692
</div>
8793
</div>
8894
</div>

frontend/src/app/store/[id]/StoreContent.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
'use client';
22

33
import { useState, useEffect } from 'react';
4-
import { useAuth } from '../../contexts/auth';
54
import { useCart } from '../../contexts/cart';
65
import { toast } from 'react-hot-toast';
76
import { FaInstagram, FaFacebook, FaXTwitter } from 'react-icons/fa6';
87
import { SiBluesky } from 'react-icons/si';
9-
import Link from 'next/link';
108
import { ClockIcon, MapPinIcon } from '@heroicons/react/24/outline';
119
import { config } from '@/config';
1210

@@ -40,7 +38,6 @@ export default function StoreContent({ storeId }: { storeId: string }) {
4038

4139
// Check if cart has items from a different store
4240
const hasItemsFromDifferentStore = cartItems.length > 0 && cartItems[0].store !== storeId;
43-
const currentCartStoreName = hasItemsFromDifferentStore ? store?.name : null;
4441

4542
useEffect(() => {
4643
const fetchStoreAndItems = async () => {
@@ -59,13 +56,11 @@ export default function StoreContent({ storeId }: { storeId: string }) {
5956
throw new Error('Failed to fetch store items');
6057
}
6158
const itemsData = await itemsResponse.json();
62-
6359
// Add mock image URLs to items
6460
const itemsWithImages = itemsData.map((item: StoreItem) => ({
6561
...item,
6662
imageUrl: `https://picsum.photos/seed/${item.id}/400/300`
6763
}));
68-
6964
setItems(itemsWithImages);
7065
} catch (err) {
7166
setError(err instanceof Error ? err.message : 'Failed to fetch store data');
@@ -222,4 +217,4 @@ export default function StoreContent({ storeId }: { storeId: string }) {
222217
</div>
223218
</main>
224219
);
225-
}
220+
}

0 commit comments

Comments
 (0)