Skip to content

Commit 7a4c161

Browse files
authored
Merge pull request NCUAppTeam#152 from 1989ONCE/dinner
feat: dinner page major update
2 parents ee32368 + 26f880a commit 7a4c161

File tree

16 files changed

+672
-2
lines changed

16 files changed

+672
-2
lines changed

src/assets/forest.jpg

8.06 MB
Loading
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import ErrorHandler from "../../../utils/ErrorHandler";
2+
import { supabase } from "../../../utils/supabase";
3+
4+
import Restaurant, { DBRestaurant } from '../Entities/Restaurant';
5+
import RestaurantService from '../Services/RestaurantService';
6+
7+
8+
const RESTAURANT_TABLE_NAME = "restaurants"
9+
10+
11+
export default class RestaurantController {
12+
13+
/**
14+
* Get an array of restaurants
15+
*
16+
* @usage restaurantConroller.getAllRestaurants(<PARAMS>).then(
17+
* (users: Array<Restaurant>) => { ... }
18+
* )
19+
*
20+
* @param {string} fields - The columns to retrieve (comma-separated)
21+
* @param {string} orderBy - Which field to order by (leave blank if not needed)
22+
* @param {boolean} orderDescending - Whether to order in descending order (defaults to false)
23+
*
24+
* @returns {Array<Restaurant>} - Array of restaurants
25+
*
26+
* @see [https://supabase.com/docs/reference/javascript/order]
27+
* @see [https://supabase.com/docs/reference/javascript/range]
28+
*
29+
* @author Susan Chen. (@1989ONCE)
30+
*/
31+
public async getAllRestaurants(
32+
fields: string,
33+
orderBy?: string,
34+
orderDescending?: boolean,
35+
) : Promise<Array<Restaurant> | null> {
36+
37+
const query = supabase
38+
.from(RESTAURANT_TABLE_NAME)
39+
.select(fields)
40+
.returns<Array<DBRestaurant>>()
41+
42+
if (orderBy)
43+
query.order(orderBy, { ascending: !orderDescending })
44+
45+
const { data, error } = await query
46+
47+
// Error handling
48+
if (error) {
49+
ErrorHandler.handleSupabaseError(error)
50+
return null
51+
}
52+
53+
// Initialize result array
54+
const restaurants : Array<Restaurant> = []
55+
56+
// For each found DBRestaurant, convert to Restaurant and append to result array
57+
data.forEach((record: DBRestaurant) => {
58+
restaurants.push(
59+
RestaurantService.parseRestaurant(record)
60+
)
61+
})
62+
63+
return restaurants
64+
}
65+
66+
67+
68+
/**
69+
* Find a single restaurant by ID
70+
*
71+
* @usage restaurantController.findRestaurantByID(<PARAMS>).then(
72+
* (restaurant: Restaurant) => { ... }
73+
* )
74+
*
75+
* @param {string} restaurantID - Target restaurant ID
76+
* @param {string} fields - The columns to retrieve
77+
*
78+
* @returns {User} - The target user entity (null if not found)
79+
*
80+
* @author Susan Chen. (@1989ONCE)
81+
*/
82+
public async findRestaurantByID(restaurantID: string, fields?: string) : Promise<Restaurant | null> {
83+
84+
const { data, error } = await supabase
85+
.from(RESTAURANT_TABLE_NAME)
86+
.select(fields)
87+
.eq("id", restaurantID)
88+
.returns<DBRestaurant>()
89+
.limit(1)
90+
.single()
91+
92+
93+
// Error handling
94+
if (error) {
95+
ErrorHandler.handleSupabaseError(error)
96+
return null
97+
}
98+
99+
if (!data)
100+
return null
101+
102+
// Type conversion: DBUser -> User
103+
const restaurant : Restaurant = RestaurantService.parseRestaurant(data)
104+
105+
return restaurant
106+
}
107+
108+
109+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Database } from "../../../utils/database.types";
2+
3+
/**
4+
* This is a dummy-type inherited from the generated Supabase type
5+
*/
6+
export type DBRestaurant = Database['public']['Tables']['restaurants']['Row'];
7+
8+
9+
export default class Restaurant {
10+
11+
public id: number = 0;
12+
public openhr: string = "";
13+
public address: string = "";
14+
public location: number = 0;
15+
public openday: Array<number> = [];
16+
public restaurant: string = "";
17+
public fk_category: number = 0;
18+
public image: string = "";
19+
public menu: string = "";
20+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
import Restaurant, { DBRestaurant } from "../Entities/Restaurant";
3+
4+
const RestaurantService = {
5+
6+
/**
7+
* Convert a restaurant from default Supabase type to User entity
8+
*
9+
* @param {DBRestaurant} record - The record retrieved from Supabase
10+
* @returns {Restaurant} - Converted user entity
11+
*
12+
* @author Susan Chen (@1989ONCE)
13+
*/
14+
parseRestaurant(record: DBRestaurant) : Restaurant {
15+
if (!record || typeof record !== 'object')
16+
throw new Error('Invalid record provided')
17+
18+
if (!record.id)
19+
throw new Error('id is a required field')
20+
21+
const restaurant = new Restaurant()
22+
23+
restaurant.id = record.id
24+
restaurant.openhr = record.openhr ?? "以店家公告為主"
25+
restaurant.address = record.address ?? "-"
26+
restaurant.location = record.location ?? 0
27+
restaurant.openday = record.openday ?? []
28+
restaurant.restaurant = record.restaurant ?? "-"
29+
restaurant.fk_category = record.fk_category ?? 0
30+
restaurant.image = record.image ?? "https://via.placeholder.com/150"
31+
restaurant.menu = record.menu ?? "https://via.placeholder.com/150"
32+
33+
return restaurant
34+
}
35+
36+
}
37+
38+
export default RestaurantService

src/components/DrawerSideBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const signOut = async () => {
1111

1212
export const DrawerSideBar = ({ name, avatar }: { name: string, avatar: string }) => {
1313
return (
14-
<div className="flex w-fit drawer">
14+
<div className="flex w-fit drawer z-10">
1515
<input id="my-drawer" type="checkbox" className="drawer-toggle" />
1616

1717

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Restaurant from "../../../backend/dinner/Entities/Restaurant";
2+
3+
export function DinnerDetail({
4+
restaurant,
5+
closeDetail,
6+
}: {
7+
restaurant: Restaurant | null;
8+
closeDetail: () => void;
9+
}) {
10+
return (
11+
<dialog
12+
id="dinner_detail_modal"
13+
className="modal"
14+
open={!!restaurant}
15+
>
16+
<div className="modal-box max-h-[80vh] overflow-y-auto relative">
17+
<form method="dialog">
18+
<button
19+
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
20+
onClick={closeDetail}
21+
>
22+
23+
</button>
24+
</form>
25+
{restaurant && (
26+
<>
27+
<h3 className="font-bold text-lg">{restaurant.restaurant}</h3>
28+
<p className="py-2">
29+
<strong>地址:</strong> {restaurant.address}
30+
</p>
31+
<p className="py-2">
32+
<strong>營業時間:</strong> {restaurant.openhr}
33+
</p>
34+
<p className="py-2">
35+
<strong>營業日:</strong> {restaurant.openday.join(", ")}
36+
</p>
37+
<p className="py-2">
38+
<strong>分類:</strong> {restaurant.fk_category}
39+
</p>
40+
<img
41+
className="w-full h-48 object-cover rounded-lg mt-4"
42+
src={restaurant.image}
43+
alt={restaurant.restaurant}
44+
/>
45+
</>
46+
)}
47+
</div>
48+
<label
49+
className="modal-backdrop"
50+
htmlFor="dinner_detail_modal"
51+
onClick={closeDetail}
52+
>
53+
Close
54+
</label>
55+
</dialog>
56+
);
57+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { DocumentTextIcon, PlusCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
2+
import Restaurant from "../../../backend/dinner/Entities/Restaurant";
3+
4+
export function DinnerListCard({
5+
restaurant,
6+
toggleSelect,
7+
isSelected,
8+
openDetail,
9+
}: {
10+
restaurant: Restaurant;
11+
toggleSelect: (restaurant: Restaurant) => void;
12+
isSelected: boolean;
13+
openDetail: (restaurant: Restaurant) => void;
14+
}) {
15+
return (
16+
<div className="flex flex-row card card-side bg-white shadow-sm h-24 my-4 shadow-xl rounded-lg">
17+
{/* Image Section */}
18+
<div className="flex-shrink-0 w-24 h-24">
19+
<img
20+
className="object-cover w-full h-full border-2 border-gray-200 rounded-l-lg"
21+
src={restaurant.image}
22+
alt={restaurant.restaurant}
23+
/>
24+
</div>
25+
26+
{/* Title Section */}
27+
<div className="flex-grow w-1/3 flex flex-col justify-center px-4">
28+
<h4 className="card-title text-sm font-semibold break-words">
29+
{restaurant.restaurant}
30+
</h4>
31+
</div>
32+
33+
{/* Button Section */}
34+
<div className="flex-shrink-0 w-1/3 bg-gray-200 flex flex-row justify-center items-center rounded-r-lg px-4">
35+
<div className="card-actions flex flex-col sm:flex-row items-center space-y-2 sm:space-y-0 sm:space-x-2">
36+
<button
37+
className={`btn btn-sm sm:btn-md ${isSelected ? "bg-rose-100 hover:bg-rose-200 text-rose-700 hover:text-rose-800" : "bg-green-600 hover:bg-green-500 text-white"}`}
38+
onClick={() => toggleSelect(restaurant)}
39+
>
40+
{isSelected ?
41+
<div className="flex flex-row items-center">
42+
<XCircleIcon className="inline-block w-4 h-4 mr-1" />
43+
移除
44+
</div> :
45+
<div className="flex flex-row items-center">
46+
<PlusCircleIcon className="inline-block w-4 h-4 mr-1" />
47+
加入
48+
</div>
49+
}
50+
</button>
51+
<button
52+
className="btn btn-sm sm:btn-md bg-sky-200 hover:bg-sky-600 text-sky-700 hover:text-white"
53+
onClick={() => openDetail(restaurant)}
54+
>
55+
<div className="flex flex-row items-center">
56+
<DocumentTextIcon className="inline-block w-4 h-4 mr-1" />
57+
詳情
58+
</div>
59+
</button>
60+
</div>
61+
</div>
62+
</div>
63+
);
64+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import forestImage from "../../../assets/forest.jpg";
2+
3+
export function DinnerStory() {
4+
return (
5+
<div className="card bg-base-100 image-full w-full max-w-4xl h-64 sm:h-80 md:h-96 lg:h-[28rem] shadow-sm mx-auto my-6 overflow-hidden">
6+
<figure className="w-full h-full">
7+
<img
8+
src={forestImage}
9+
alt="Forest"
10+
className="object-cover w-full h-full"
11+
/>
12+
</figure>
13+
<div className="card-body p-4 sm:p-6 md:p-8">
14+
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold ">綠洲拾橡</h2>
15+
<h5 className="text-md sm:text-lg md:text-xl font-medium -translate-y-1 sm:-translate-y-2">Acorn Trails in the Oasis</h5>
16+
17+
<p className="text-[12px] sm:text-base md:text-lg text-right">
18+
當飢餓的松鼠走過乾渴的沙漠,<br />終於找到了滋養的綠洲...
19+
</p>
20+
<p className="text-[12px] sm:text-base md:text-lg text-left">
21+
<span className="underline">功能說明</span><br />
22+
★隨機挑選附近的餐廳,讓每餐都充滿著探索的樂趣和驚喜<br />
23+
★詳細餐廳資訊,讓你輕鬆掌握周邊美食<br />
24+
</p>
25+
<p className="text-[12px] sm:text-base md:text-lg text-right">
26+
無論是為了迅速決定吃什麼,還是發掘新餐廳,<br />「綠洲拾橡」都是你探索美味世界的最佳夥伴
27+
</p>
28+
</div>
29+
</div>
30+
);
31+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ISpinWheelProps, SpinWheel } from 'spin-wheel-game';
2+
export const MySpinWheel = () => {
3+
4+
const segments = [
5+
{ segmentText: 'Option 1', segColor: 'red' },
6+
{ segmentText: 'Option 2', segColor: 'blue' },
7+
{ segmentText: 'Option 3', segColor: 'green' },
8+
// Add more segments as needed
9+
];
10+
11+
const handleSpinFinish = (result: string) => {
12+
console.log(`Spun to: ${result}`);
13+
// Handle the result as needed
14+
};
15+
16+
const spinWheelProps: ISpinWheelProps = {
17+
segments,
18+
onFinished: handleSpinFinish,
19+
primaryColor: 'black',
20+
contrastColor: 'white',
21+
buttonText: 'Spin',
22+
isOnlyOnce: false,
23+
size: 290,
24+
upDuration: 100,
25+
downDuration: 600,
26+
fontFamily: 'Arial',
27+
arrowLocation: 'top',
28+
showTextOnSpin: true,
29+
isSpinSound: true,
30+
};
31+
32+
return (
33+
<div className='mx-auto mt-4'>
34+
<h4>請選擇5-10家餐廳作為輪盤選項</h4>
35+
36+
<SpinWheel {...spinWheelProps} />
37+
</div >
38+
);
39+
40+
};

0 commit comments

Comments
 (0)