diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e8b1539b..9812d427 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -124,7 +124,7 @@ ### Entry + Build - Entry: `dashboard/src/main.js` mounts `App.vue` with router, resources, translation plugin, and socket. - Build: `dashboard/vite.config.js` outputs to `buzz/public/dashboard` and updates `buzz/www/dashboard.html`. -- Base URL: `/dashboard` (router history uses `createWebHistory("/dashboard")`). +- Base URL: `/b` (router history uses `createWebHistory("/b")`). Old `/dashboard/*` links 301-redirect to `/b/*` via `website_redirects` in `hooks.py`. ### Routing - Public-like flows: booking (`/book-tickets/:eventRoute`) and check-in (`/check-in`). @@ -237,7 +237,7 @@ erDiagram - Update frontend rendering in `CustomFieldInput.vue` and `BookingForm.vue`. - Payment flow changes - Update `buzz/payments.py` and any event-scoped gateway selection logic. - - Ensure payment redirects still land on `/dashboard/...?...success=true`. + - Ensure payment redirects still land on `/b/...?...success=true`. - Sponsorship flow changes - Update `Sponsorship Enquiry` for status transitions and `SponsorshipDetails.vue` for UI state. - Verify sponsor creation in `on_payment_authorized`. diff --git a/buzz/api/__init__.py b/buzz/api/__init__.py index 1afe277f..20cb4a02 100644 --- a/buzz/api/__init__.py +++ b/buzz/api/__init__.py @@ -515,7 +515,7 @@ def process_booking( return { "payment_link": get_payment_link_for_booking( booking.name, - redirect_to=f"/dashboard/bookings/{booking.name}?success=true", + redirect_to=f"/b/bookings/{booking.name}?success=true", payment_gateway=payment_gateway, ) } @@ -849,7 +849,7 @@ def create_sponsorship_payment_link(enquiry_id: str, tier_id: str, payment_gatew if enquiry.owner != frappe.session.user: frappe.throw(frappe._("Not permitted to create payment for this enquiry")) - redirect_url = f"/dashboard/account/sponsorships/{enquiry_id}?success=true" + redirect_url = f"/b/account/sponsorships/{enquiry_id}?success=true" return get_payment_link_for_sponsorship( enquiry_id, tier_id, redirect_url, payment_gateway=payment_gateway ) diff --git a/buzz/api/auth.py b/buzz/api/auth.py index a1335646..d5b3ce91 100644 --- a/buzz/api/auth.py +++ b/buzz/api/auth.py @@ -16,7 +16,7 @@ def get_login_context(redirect_to: str | None = None): } if not redirect_to: - redirect_to = frappe.utils.get_url("/dashboard") + redirect_to = frappe.utils.get_url("/b") social_login_keys = frappe.get_all( "Social Login Key", diff --git a/buzz/buzz_marketing/doctype/buzz_campaign/buzz_campaign.py b/buzz/buzz_marketing/doctype/buzz_campaign/buzz_campaign.py index e95827f8..5f7fa662 100644 --- a/buzz/buzz_marketing/doctype/buzz_campaign/buzz_campaign.py +++ b/buzz/buzz_marketing/doctype/buzz_campaign/buzz_campaign.py @@ -41,7 +41,7 @@ def validate_crm_installed(self): frappe.throw(_("Please install Frappe CRM to use campaigns feature")) def generate_qr_code(self): - register_url = f"{frappe.utils.get_url()}/dashboard/register-interest/{self.name}" + register_url = f"{frappe.utils.get_url()}/b/register-interest/{self.name}" self.qr_code = generate_qr_code_file( doc=self, data=register_url, diff --git a/buzz/events/doctype/buzz_event/buzz_event.js b/buzz/events/doctype/buzz_event/buzz_event.js index 31330538..c5dfcf6f 100644 --- a/buzz/events/doctype/buzz_event/buzz_event.js +++ b/buzz/events/doctype/buzz_event/buzz_event.js @@ -292,7 +292,7 @@ function show_save_as_template_dialog(frm) { frappe.ui.form.on("Buzz Event Form", { copy_to_clipboard(frm, cdt, cdn) { const row = frappe.get_doc(cdt, cdn); - const url = `${window.location.origin}/dashboard/events/${frm.doc.route}/forms/${row.route}`; + const url = `${window.location.origin}/b/${frm.doc.route}/${row.route}`; navigator.clipboard.writeText(url); frappe.show_alert({ message: __("Link copied!"), indicator: "green" }); }, @@ -307,11 +307,11 @@ frappe.ui.form.on("Buzz Event", { } if (frm.doc.route) { - frm.add_web_link(`/dashboard/book-tickets/${frm.doc.route}`, "View Registration Page"); + frm.add_web_link(`/b/register/${frm.doc.route}`, "View Registration Page"); } if (!frm.is_new()) { - frm.add_web_link(`/dashboard/check-in/${frm.doc.name}`, __("Open Check-in")); + frm.add_web_link(`/b/check-in/${frm.doc.name}`, __("Open Check-in")); } const button_label = frm.doc.is_published ? __("Unpublish") : __("Publish"); diff --git a/buzz/events/doctype/buzz_event/buzz_event.py b/buzz/events/doctype/buzz_event/buzz_event.py index 22f9ff33..1738e112 100644 --- a/buzz/events/doctype/buzz_event/buzz_event.py +++ b/buzz/events/doctype/buzz_event/buzz_event.py @@ -10,6 +10,19 @@ from buzz.api.forms import validate_excluded_fields from buzz.utils import only_if_app_installed +# Top-level dashboard route segments (/b/) an event route must not shadow. +RESERVED_EVENT_ROUTES = { + "account", + "bookings", + "tickets", + "register", + "register-interest", + "check-in", + "book-tickets", + "event-proposal", + "events", +} + class BuzzEvent(Document): # begin: auto-generated types @@ -145,6 +158,11 @@ def validate_route(self): route = frappe.website.utils.cleanup_page_name(self.title).replace("_", "-") self.route = append_number_if_name_exists("Buzz Event", route, fieldname="route") + if self.route in RESERVED_EVENT_ROUTES: + frappe.throw( + _("'{0}' is a reserved route and cannot be used as an event route.").format(self.route) + ) + def validate_guest_verification_config(self): """Ensure email/SMS is configured when OTP verification is enabled.""" if frappe.in_test or not self.allow_guest_booking: diff --git a/buzz/hooks.py b/buzz/hooks.py index 147d5df6..d08ef09e 100644 --- a/buzz/hooks.py +++ b/buzz/hooks.py @@ -18,7 +18,15 @@ website_route_rules = [ - {"from_route": "/dashboard/", "to_route": "dashboard"}, + {"from_route": "/b/", "to_route": "dashboard"}, +] + +# Keep old /dashboard/* links working: redirect to the shortened /b/* scheme. +# Ordered specific -> catch-all; the first matching source wins. +website_redirects = [ + {"source": r"/dashboard/events/([^/]+)/forms/([^/]+)", "target": r"/b/\1/\2"}, + {"source": r"/dashboard/book-tickets/(.+)", "target": r"/b/register/\1"}, + {"source": r"/dashboard/(.*)", "target": r"/b/\1"}, ] # Scheduled Tasks diff --git a/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py b/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py index 65d899fc..eb5975e7 100644 --- a/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py +++ b/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py @@ -115,7 +115,7 @@ def send_pitch_deck(self, now=False): def send_approval_notification(self): event = frappe.get_cached_doc("Buzz Event", self.event) host_name = event.host or "The Event Team" - dashboard_link = get_url(f"/dashboard/account/sponsorships/{self.name}") + dashboard_link = get_url(f"/b/account/sponsorships/{self.name}") subject = f"[Payment Pending] Your Sponsorship for {event.title} has been Approved!" message = f""" diff --git a/buzz/ticketing/doctype/event_ticket/event_ticket.py b/buzz/ticketing/doctype/event_ticket/event_ticket.py index 9f5f6528..9b55e0b5 100644 --- a/buzz/ticketing/doctype/event_ticket/event_ticket.py +++ b/buzz/ticketing/doctype/event_ticket/event_ticket.py @@ -87,7 +87,7 @@ def send_user_invitation(self): invite_by_email( emails=self.attendee_email, roles=["Buzz User"], - redirect_to_path="/dashboard/account/tickets", + redirect_to_path="/b/account/tickets", app_name="buzz", ) diff --git a/dashboard/src/router.ts b/dashboard/src/router.ts index a782dfcf..57011067 100644 --- a/dashboard/src/router.ts +++ b/dashboard/src/router.ts @@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [ component: () => import("@/pages/CheckInScanner.vue"), }, { - path: "/book-tickets/:eventRoute", + path: "/register/:eventRoute", props: true, name: "event-booking", meta: { isPublic: true }, @@ -26,19 +26,21 @@ const routes: RouteRecordRaw[] = [ meta: { isPublic: true }, component: () => import("@/pages/EventProposalForm.vue"), }, - { - path: "/events/:eventRoute/forms/:formRoute", - props: true, - name: "custom-form", - meta: { isPublic: true }, - component: () => import("@/pages/CustomFormPage.vue"), - }, { path: "/register-interest/:campaign", props: true, name: "register-interest", component: () => import("@/pages/RegisterInterest.vue"), }, + // Back-compat: old in-app paths redirect to the shortened scheme. + { + path: "/book-tickets/:eventRoute", + redirect: (to) => ({ name: "event-booking", params: to.params }), + }, + { + path: "/events/:eventRoute/forms/:formRoute", + redirect: (to) => ({ name: "custom-form", params: to.params }), + }, { path: "/bookings", name: "bookings-tab", @@ -113,10 +115,19 @@ const routes: RouteRecordRaw[] = [ }, ], }, + // Event custom form: /b//
. Declared last — two dynamic segments, + // so it only matches after every static route above has been ruled out. + { + path: "/:eventRoute/:formRoute", + props: true, + name: "custom-form", + meta: { isPublic: true }, + component: () => import("@/pages/CustomFormPage.vue"), + }, ] const router = createRouter({ - history: createWebHistory("/dashboard"), + history: createWebHistory("/b"), routes, }) diff --git a/e2e/pages/booking.page.ts b/e2e/pages/booking.page.ts index 3f3dbbaf..9163d122 100644 --- a/e2e/pages/booking.page.ts +++ b/e2e/pages/booking.page.ts @@ -37,7 +37,7 @@ export class BookingPage { // Navigate to the booking page for a specific event. async goto(eventRoute: string): Promise { - await this.page.goto(`/dashboard/book-tickets/${eventRoute}`); + await this.page.goto(`/b/register/${eventRoute}`); await this.page.waitForLoadState("networkidle"); } diff --git a/e2e/pages/custom-form.page.ts b/e2e/pages/custom-form.page.ts index 217110fa..545c9db6 100644 --- a/e2e/pages/custom-form.page.ts +++ b/e2e/pages/custom-form.page.ts @@ -18,7 +18,7 @@ export class CustomFormPage { } async goto(eventRoute: string, formRoute: string): Promise { - await this.page.goto(`/dashboard/events/${eventRoute}/forms/${formRoute}`); + await this.page.goto(`/b/${eventRoute}/${formRoute}`); await this.page.waitForLoadState("networkidle"); } diff --git a/e2e/pages/event-proposal.page.ts b/e2e/pages/event-proposal.page.ts index ec6b3dec..4a925011 100644 --- a/e2e/pages/event-proposal.page.ts +++ b/e2e/pages/event-proposal.page.ts @@ -16,7 +16,7 @@ export class EventProposalPage { } async goto(): Promise { - await this.page.goto("/dashboard/event-proposal"); + await this.page.goto("/b/event-proposal"); await this.page.waitForLoadState("networkidle"); } diff --git a/e2e/tests/auth.spec.ts b/e2e/tests/auth.spec.ts index b6d4823e..eea1c338 100644 --- a/e2e/tests/auth.spec.ts +++ b/e2e/tests/auth.spec.ts @@ -8,7 +8,7 @@ import { isLoggedIn } from "../helpers"; test.describe("Authentication - Pre-authenticated", () => { test("should access buzz when authenticated", async ({ page }) => { // Already authenticated via setup project - await page.goto("/dashboard/"); + await page.goto("/b/"); await page.waitForLoadState("networkidle"); // Should not be redirected to login @@ -51,7 +51,7 @@ test.describe("Authentication - Fresh state", () => { }); test("should show login button when not authenticated", async ({ page }) => { - await page.goto("/dashboard"); + await page.goto("/b"); await page.waitForLoadState("networkidle"); await expect(page.getByRole("button", { name: "Log In" }).first()).toBeVisible(); diff --git a/e2e/tests/event.setup.ts b/e2e/tests/event.setup.ts index 90af6ef9..4f64fa8a 100644 --- a/e2e/tests/event.setup.ts +++ b/e2e/tests/event.setup.ts @@ -109,5 +109,5 @@ setup("create test event for booking", async ({ request }) => { }); console.log(`Created Ticket Add-on: ${addOn.name}`); - console.log(`Test event setup complete! Route: /dashboard/book-tickets/${testEventRoute}`); + console.log(`Test event setup complete! Route: /b/register/${testEventRoute}`); }); diff --git a/e2e/tests/login-modal.spec.ts b/e2e/tests/login-modal.spec.ts index b5593b91..b578a479 100644 --- a/e2e/tests/login-modal.spec.ts +++ b/e2e/tests/login-modal.spec.ts @@ -7,7 +7,7 @@ test.describe("Login Modal", () => { test.use({ storageState: { cookies: [], origins: [] } }); test("email/password login via modal", async ({ page }) => { - await page.goto("/dashboard"); + await page.goto("/b"); await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Log In" }).first().click(); await expect(page.getByRole("dialog")).toBeVisible(); @@ -20,7 +20,7 @@ test.describe("Login Modal", () => { }); test("shows error for invalid credentials", async ({ page }) => { - await page.goto("/dashboard"); + await page.goto("/b"); await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Log In" }).first().click(); diff --git a/e2e/tests/offline-payment.spec.ts b/e2e/tests/offline-payment.spec.ts index 7e553ae6..c4b83b5b 100644 --- a/e2e/tests/offline-payment.spec.ts +++ b/e2e/tests/offline-payment.spec.ts @@ -74,7 +74,7 @@ test.describe("Booking Details - Offline Payment", () => { test("shows verification pending status", async ({ page }) => { // This test assumes a booking already exists // Navigate to bookings list - await page.goto("/dashboard/bookings"); + await page.goto("/b/bookings"); await page.waitForLoadState("networkidle"); // Look for verification pending badge