diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 596347e..2f0727e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -70,4 +70,4 @@ members of the project's leadership. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -[homepage]: https://www.contributor-covenant.org \ No newline at end of file +[homepage]: https://www.contributor-covenant.org diff --git a/README.md b/README.md index 7502dcd..8f9f058 100644 --- a/README.md +++ b/README.md @@ -8,32 +8,32 @@ Twilio Mixologist is an application that allows you to solve the problem of long queues at stands at events. Attendees can order their coffee, smoothie or whatever you serve via Twilio-powered channels, Mixologists get all orders on a website that can be accessed via a tablet and once an order is done the attendee will be notified via the system to come and pick it up. No more queueing and efficient coffee ☕️ ordering! 🎉 If you want to learn more about how this project was started, check out the this blog post: -> [Serving Coffee with Twilio Programmable SMS and React](https://www.twilio.com/en-us/blog/serving-coffee-with-sms-and-react-html) -Different versions of this system have been used at events such as: - -* [NDC Oslo](https://ndcoslo.com) 2016, 2017 -* [CSSConf EU](https://2017.cssconf.eu/) && [JSConf EU](https://2017.jsconf.eu/) 2017 -* [WeAreDevelopers World Congress](https://www.wearedevelopers.com/world-congress) 2023, 2024 -* [Mobile World Congress Barcelona](https://www.mwcbarcelona.com/) 2023, 2024 -* [Money 20/20](https://www.money2020.com/) 2023 -* [Twilio SIGNAL](https://signal.twilio.com/) 2023, 2024 +> [Serving Coffee with Twilio Programmable SMS and React](https://www.twilio.com/en-us/blog/serving-coffee-with-sms-and-react-html) +Different versions of this system have been used at events such as: +- [NDC Oslo](https://ndcoslo.com) 2016, 2017 +- [CSSConf EU](https://2017.cssconf.eu/) && [JSConf EU](https://2017.jsconf.eu/) 2017 +- [WeAreDevelopers World Congress](https://www.wearedevelopers.com/world-congress) 2023, 2024 +- [Mobile World Congress Barcelona](https://www.mwcbarcelona.com/) 2023, 2024 +- [Money 20/20](https://www.money2020.com/) 2023 +- [Twilio SIGNAL](https://signal.twilio.com/) 2023, 2024 ## Features -* Receive orders using [Twilio Messaging] -* Store orders and real-time synchronization them between back-end and front-end using [Twilio Sync] -* Easy dynamic application configuration using [Twilio Sync] -* Managing message threads using [Twilio Conversations] -* Permission management based on [Twilio Sync] -* Easy way to reset the application from the admin interface -* Support multiple events that happen in parallel -* Query for location in the queue as well as canceling the order as a user -* All combined into a single [NextJS](https://nextjs.org/) web application +- Receive orders using [Twilio Messaging] +- Store orders and real-time synchronization them between back-end and front-end using [Twilio Sync] +- Easy dynamic application configuration using [Twilio Sync] +- Managing message threads using [Twilio Conversations] +- Permission management based on [Twilio Sync] +- Easy way to reset the application from the admin interface +- Support multiple events that happen in parallel +- Query for location in the queue as well as canceling the order as a user +- All combined into a single [NextJS](https://nextjs.org/) web application ### Pending Features + - [ ] Integration with Segment - [ ] Your suggestions @@ -41,17 +41,16 @@ Different versions of this system have been used at events such as: The current [Twilio Channels] are: -* [WhatsApp][twilio whatsapp] -* [SMS][twilio messaging] - +- [WhatsApp][twilio whatsapp] +- [SMS][twilio messaging] ## Setup ### Requirements -* [Node.js] version 20 or higher -* [pnpm] -* A Twilio account - [Sign up here](https://www.twilio.com/try-twilio) +- [Node.js] version 20 or higher +- [pnpm] +- A Twilio account - [Sign up here](https://www.twilio.com/try-twilio) ## Setup @@ -136,13 +135,12 @@ All third party contributors acknowledge that any contributions they provide wil ## Icons Used -* [Mixologist Icons by Oliver Pitsch](https://www.smashingmagazine.com/2016/03/freebie-Mixologist-iconset-50-icons-eps-png-svg/) -* [Bar by BirVa Mehta from Noun Project](https://thenounproject.com/term/bar/1323725/) +- [Mixologist Icons by Oliver Pitsch](https://www.smashingmagazine.com/2016/03/freebie-Mixologist-iconset-50-icons-eps-png-svg/) +- [Bar by BirVa Mehta from Noun Project](https://thenounproject.com/term/bar/1323725/) ## License -MIT - +MIT [twilio console]: https://www.twilio.com/console [twilio rest api]: https://www.twilio.com/docs/api/rest diff --git a/__tests__/e2e/browse-orders.spec.ts b/__tests__/e2e/browse-orders.spec.ts index 60f96d7..1182940 100644 --- a/__tests__/e2e/browse-orders.spec.ts +++ b/__tests__/e2e/browse-orders.spec.ts @@ -165,13 +165,13 @@ test.describe("[mixologist]", () => { await page.goto("/event/test-event/orders"); - await page - .getByRole("button", { name: "Create a Manual Order" }) - .click(); + await page.getByRole("button", { name: "Create a Manual Order" }).click(); await page.getByPlaceholder("Attendee name").fill("Test Name"); await page.getByLabel("Order Item").click(); - await page.getByLabel('Espresso', { exact: true }).click(); - await page.getByPlaceholder("Without regular milk or similar...").fill("Test Notes"); + await page.getByLabel("Espresso", { exact: true }).click(); + await page + .getByPlaceholder("Without regular milk or similar...") + .fill("Test Notes"); await page .getByRole("button", { name: "Create Order", exact: true }) .click(); @@ -179,4 +179,4 @@ test.describe("[mixologist]", () => { page.getByRole("button", { name: "Creating...", exact: true }), ).toBeVisible(); }); -}); \ No newline at end of file +}); diff --git a/src/app/api/[slug]/broadcast/route.ts b/src/app/api/[slug]/broadcast/route.ts index 1461acc..64f824a 100644 --- a/src/app/api/[slug]/broadcast/route.ts +++ b/src/app/api/[slug]/broadcast/route.ts @@ -43,9 +43,9 @@ export async function POST( listItem.data?.status === "queued" || listItem.data?.status === "ready", ); - queuedOrders.forEach(order => { - addMessageToConversation(order.data.key, message) - }) + queuedOrders.forEach((order) => { + addMessageToConversation(order.data.key, message); + }); return new Response(null, { status: 201 }); } catch (e: any) { diff --git a/src/app/api/broadcast/route.ts b/src/app/api/broadcast/route.ts index a98e717..3d3355b 100644 --- a/src/app/api/broadcast/route.ts +++ b/src/app/api/broadcast/route.ts @@ -1,19 +1,18 @@ - - import { headers } from "next/headers"; -import { - fetchSyncListItems, - addMessageToConversation -} from "@/lib/twilio"; +import { fetchSyncListItems, addMessageToConversation } from "@/lib/twilio"; import { Privilege, getAuthenticatedRole } from "@/middleware"; export async function POST(request: Request) { -const headersList = headers(); - const role = getAuthenticatedRole(headersList.get("Authorization") || ""); + const headersList = headers(); + const role = getAuthenticatedRole(headersList.get("Authorization") || ""); - const hasPermissions = Privilege.ADMIN === role || Privilege.MIXOLOGIST === role; - if (!process.env.NEXT_PUBLIC_EVENTS_MAP || !process.env.NEXT_PUBLIC_ACTIVE_CUSTOMERS_MAP ) { + const hasPermissions = + Privilege.ADMIN === role || Privilege.MIXOLOGIST === role; + if ( + !process.env.NEXT_PUBLIC_EVENTS_MAP || + !process.env.NEXT_PUBLIC_ACTIVE_CUSTOMERS_MAP + ) { console.error("No config doc specified"); return new Response("No config doc specified", { status: 500, @@ -31,16 +30,20 @@ const headersList = headers(); ); } - const { event, message } = await request.json(); - try { - const listItems = await fetchSyncListItems(event); - const queuedOrders = listItems.filter(listItem => (listItem.data?.status === 'queued' || listItem.data?.status === 'ready') && !listItem.data?.manual); - - queuedOrders.forEach(order => { - addMessageToConversation(order.data.key, message); - }) - return new Response(null, {status: 201}) + const { event, message } = await request.json(); + try { + const listItems = await fetchSyncListItems(event); + const queuedOrders = listItems.filter( + (listItem) => + (listItem.data?.status === "queued" || + listItem.data?.status === "ready") && + !listItem.data?.manual, + ); + queuedOrders.forEach((order) => { + addMessageToConversation(order.data.key, message); + }); + return new Response(null, { status: 201 }); } catch (e: any) { console.error(e); return new Response(e.message, { status: 500, statusText: e.message }); diff --git a/src/app/api/event/[slug]/stats/route.ts b/src/app/api/event/[slug]/stats/route.ts index fd5abbe..bf60f11 100644 --- a/src/app/api/event/[slug]/stats/route.ts +++ b/src/app/api/event/[slug]/stats/route.ts @@ -8,7 +8,7 @@ export async function GET( request: Request, { params }: { params: { slug: string } }, ): Promise { - const headersList = headers(); + const headersList = headers(); const role = getAuthenticatedRole(headersList.get("Authorization") || ""); if (role !== Privilege.ADMIN) { diff --git a/src/app/event/[slug]/orders/[terminal]/page.tsx b/src/app/event/[slug]/orders/[terminal]/page.tsx index cda011d..6797f21 100644 --- a/src/app/event/[slug]/orders/[terminal]/page.tsx +++ b/src/app/event/[slug]/orders/[terminal]/page.tsx @@ -13,14 +13,23 @@ export default function TerminalPage({ const terminalId = Number(matchedGroups?.[1]), terminalCount = Number(matchedGroups?.[2]); - if (!terminalRegex.test(terminal) || terminalId > terminalCount || terminalId === 0 || isNaN(terminalId) || isNaN(terminalCount)) { + if ( + !terminalRegex.test(terminal) || + terminalId > terminalCount || + terminalId === 0 || + isNaN(terminalId) || + isNaN(terminalCount) + ) { return notFound(); } - return (
- +
); } diff --git a/src/app/event/[slug]/orders/ordersList.tsx b/src/app/event/[slug]/orders/ordersList.tsx index 65d3db4..21093f2 100644 --- a/src/app/event/[slug]/orders/ordersList.tsx +++ b/src/app/event/[slug]/orders/ordersList.tsx @@ -18,12 +18,7 @@ import { getOrderReadyReminderMessage, } from "@/lib/templates"; -import { - Check, - Trash2Icon, - BellRing, - UserCheck, -} from "lucide-react"; +import { Check, Trash2Icon, BellRing, UserCheck } from "lucide-react"; export default function OrdersList({ ordersList, diff --git a/src/app/webhooks/conversations/route.ts b/src/app/webhooks/conversations/route.ts index c7a1ac2..201e30e 100644 --- a/src/app/webhooks/conversations/route.ts +++ b/src/app/webhooks/conversations/route.ts @@ -89,13 +89,12 @@ export async function POST(request: Request) { await sleep(2000); const dataPolicy = templates.getDataPolicy(newEvent.selection.mode); addMessageToConversation(conversationSid, dataPolicy); - const message = - await templates.getReadyToOrderMessage( - newEvent, - newEvent.selection.items, - newEvent.maxOrders, - true - ); + const message = await templates.getReadyToOrderMessage( + newEvent, + newEvent.selection.items, + newEvent.maxOrders, + true, + ); addMessageToConversation( conversationSid, "", @@ -136,13 +135,12 @@ export async function POST(request: Request) { await sleep(2000); const dataPolicy = templates.getDataPolicy(newEvent.selection.mode); addMessageToConversation(conversationSid, dataPolicy); - const message = - await templates.getReadyToOrderMessage( - newEvent, - newEvent.selection.items, - newEvent.maxOrders, - true - ); + const message = await templates.getReadyToOrderMessage( + newEvent, + newEvent.selection.items, + newEvent.maxOrders, + true, + ); addMessageToConversation( conversationSid, "", @@ -184,13 +182,12 @@ export async function POST(request: Request) { newEvent.welcomeMessage, ); addMessageToConversation(conversationSid, welcomeBackMessage); - const message = - await templates.getReadyToOrderMessage( - newEvent, - newEvent.selection.items, - newEvent.maxOrders, - true - ); + const message = await templates.getReadyToOrderMessage( + newEvent, + newEvent.selection.items, + newEvent.maxOrders, + true, + ); await updateOrCreateSyncMapItem( NEXT_PUBLIC_ACTIVE_CUSTOMERS_MAP, conversationSid, @@ -235,13 +232,12 @@ export async function POST(request: Request) { ); await sleep(500); - const message = - await templates.getReadyToOrderMessage( - newEvent, - newEvent.selection.items, - newEvent.maxOrders, - true - ); + const message = await templates.getReadyToOrderMessage( + newEvent, + newEvent.selection.items, + newEvent.maxOrders, + true, + ); addMessageToConversation( conversationSid, "", @@ -355,7 +351,7 @@ export async function POST(request: Request) { event, event.selection.items, event.maxOrders, - false + false, ); addMessageToConversation( conversationSid, diff --git a/src/components/menu-item.tsx b/src/components/menu-item.tsx index 9bde4bf..bf69f85 100644 --- a/src/components/menu-item.tsx +++ b/src/components/menu-item.tsx @@ -124,7 +124,7 @@ export default function MenuItem({ className="m-2 mr-6" /> ), - "Chocolate": ( + Chocolate: ( b.shortTitle.length - a.shortTitle.length, ); for (const item in sortedItems) { - if ( - spellcheckedBody.includes( - sortedItems[item].shortTitle.toLowerCase(), - ) - ) { + if (spellcheckedBody.includes(sortedItems[item].shortTitle.toLowerCase())) { orderItem = sortedItems[item]; break; } diff --git a/src/scripts/clearOrdersForEvent.ts b/src/scripts/clearOrdersForEvent.ts index 9b5a421..4e9cb77 100644 --- a/src/scripts/clearOrdersForEvent.ts +++ b/src/scripts/clearOrdersForEvent.ts @@ -15,7 +15,9 @@ const client = twilio(TWILIO_API_KEY, TWILIO_API_SECRET, { const eventName = process.argv.pop(); if (!eventName || eventName.startsWith("/") || eventName.includes("=")) { - console.error("Please provide an event name as the last argument, e.g. 'pnpm clear-orders wearedevs24'"); + console.error( + "Please provide an event name as the last argument, e.g. 'pnpm clear-orders wearedevs24'", + ); process.exit(1); } diff --git a/src/scripts/createTwilioRes.ts b/src/scripts/createTwilioRes.ts index c239e67..8ab0716 100644 --- a/src/scripts/createTwilioRes.ts +++ b/src/scripts/createTwilioRes.ts @@ -40,7 +40,6 @@ async function createWhatsAppTemplates() { for await (const t of templates) { await deleteWhatsAppTemplate(t.sid); // Sequentially delete all templates to avoid rate limiting } - } catch (e: any) { console.error("Error deleting WhatsApp Templates ", e.message); } diff --git a/src/scripts/updateConfig.ts b/src/scripts/updateConfig.ts index 43706fe..0acf480 100644 --- a/src/scripts/updateConfig.ts +++ b/src/scripts/updateConfig.ts @@ -77,7 +77,7 @@ export async function updateConfig() { ); await configDoc.update({ data: newConfig }); } - + (async () => { - updateConfig() + updateConfig(); })();