Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d7d2067
feat(services): build service authoring workspace for USOs
vugarsafarzada Apr 25, 2026
e0c067d
refactor(services): redesign service form as full-page view matching …
vugarsafarzada Apr 25, 2026
1d7c402
fix(services): lower sticky form header z-index so settings popup ren…
vugarsafarzada Apr 25, 2026
deec173
fix(brands): lower sticky form/branch-page header z-index so settings…
vugarsafarzada Apr 25, 2026
c5ff528
feat(discovery): build public service discovery and brand detail enha…
vugarsafarzada Apr 25, 2026
2c33798
Merge pull request #16 from Reziphay/development/9
vugarsafarzada Apr 25, 2026
2af1bc8
Merge pull request #17 from Reziphay/development/10
vugarsafarzada Apr 25, 2026
efdc615
feat: implement UsoCalendarPage component with layout integration and…
vugarsafarzada Apr 26, 2026
4e1e72f
feat(moderation): build admin moderation workspace UI
vugarsafarzada Apr 26, 2026
c748d66
fix(moderation): require auth on moderation page
vugarsafarzada Apr 26, 2026
9e140ac
feat(categories): add service categories, refactor brand categories t…
vugarsafarzada Apr 26, 2026
2d63dd5
feat: introduce custom TimeInput component with format validation and…
vugarsafarzada Apr 28, 2026
664ece2
feat: increase minimum registration age to 18, add date picker compon…
vugarsafarzada Apr 28, 2026
84fb06c
feat: internationalize calendar components and provide localized date…
vugarsafarzada Apr 28, 2026
8a1eb21
refactor: simplify authentication layout background styles and update…
vugarsafarzada Apr 28, 2026
00e922c
refactor: update calendar default view and sidebar state, and adjust …
vugarsafarzada Apr 28, 2026
a4e394f
feat: hide calendar header in day view and update global styles and l…
vugarsafarzada Apr 28, 2026
81caa4c
feat: implement dynamic SEO metadata across all pages and update side…
vugarsafarzada Apr 28, 2026
52989dc
style: refactor header, theme switcher, and sidebar UI tokens to impr…
vugarsafarzada Apr 28, 2026
5725657
feat: implement collapsible sub-menus for brands and services in app-…
vugarsafarzada Apr 28, 2026
def85b6
feat: add nested branch navigation items to sidebar sub-menus
vugarsafarzada Apr 29, 2026
35f8952
feat: implement OwnerCard component and integrate owner info display …
vugarsafarzada Apr 29, 2026
b6f39db
feat: implement service and branch deletion functionality and add bra…
vugarsafarzada Apr 29, 2026
d8d91b5
refactor: replace browser prompts with custom modals for deletions an…
vugarsafarzada Apr 29, 2026
de9f059
feat: replace badge component with dynamic status pills and add brand…
vugarsafarzada Apr 29, 2026
74c0a27
Merge pull request #18 from Reziphay/development/13
vugarsafarzada Apr 29, 2026
9b17ccc
refactor: expand i18n support and improve accessibility labels across…
vugarsafarzada Apr 29, 2026
80c909a
feat: implement Russian localization support for branches, notificati…
vugarsafarzada Apr 29, 2026
67a53da
feat: update service cards with status icons, draft banners, and side…
vugarsafarzada Apr 30, 2026
21efb88
feat: implement unarchive service functionality and generalize status…
vugarsafarzada Apr 30, 2026
306b237
feat: implement rich text editor component and update moderation API …
vugarsafarzada Apr 30, 2026
ab7b3aa
feat: add social media and website link support to brands and user pr…
vugarsafarzada Apr 30, 2026
ae20a8d
feat: synchronize moderation workspace state with URL parameters and …
vugarsafarzada Apr 30, 2026
6687069
refactor: implement entity mapping for brand and service moderation w…
vugarsafarzada Apr 30, 2026
d27e345
feat: replace plain paragraph with RichTextDisplay for brand descript…
vugarsafarzada Apr 30, 2026
79f98f8
feat: add REJECTED status to sidebar dots and adjust services table g…
vugarsafarzada Apr 30, 2026
5c0d4a5
refactor: extract redundant sticky page headers into a reusable PageS…
vugarsafarzada May 1, 2026
1980fb1
feat: introduce reusable StatusBanner component and migrate existing …
vugarsafarzada May 1, 2026
1470ce3
feat: introduce StatusBadge component and refactor service status dis…
vugarsafarzada May 1, 2026
3abd8b0
feat: implement URL-based routing for service views and actions using…
vugarsafarzada May 1, 2026
0f6d6be
feat: introduce FormFooter component and standardize button usage acr…
vugarsafarzada May 1, 2026
78af81c
refactor: migrate all local copy anti-patterns to i18n system
vugarsafarzada May 1, 2026
f4097a8
refactor: remove unsupported locales, keep az/en/ru/tr only
vugarsafarzada May 1, 2026
14b1409
fix: use short label keys in service detail view instead of form fiel…
vugarsafarzada May 1, 2026
53be5b6
refactor: replace native button elements with Button component and re…
vugarsafarzada May 1, 2026
426cf58
refactor: update header and footer components with improved glassmorp…
vugarsafarzada May 3, 2026
69aeb19
feat: implement marketplace module with UCR search capabilities and i…
vugarsafarzada May 3, 2026
0573825
feat: add service rating functionality, display owner avatars, and in…
vugarsafarzada May 3, 2026
2e48787
feat: implement AccountServicesSection for displaying direct personal…
vugarsafarzada May 3, 2026
e2d16c7
feat: implement favorites functionality with API integration, state m…
vugarsafarzada May 3, 2026
531a5c5
feat: implement UcrServicesPage with infinite scrolling, category fil…
vugarsafarzada May 3, 2026
25c5d11
feat: implement marketplace search component and dedicated UCR search…
vugarsafarzada May 3, 2026
9d9e6a1
feat: optimize service loading by adding brand_id filtering, enhancin…
vugarsafarzada May 5, 2026
4c939aa
feat: integrate marketplace home sections with scrollable rails and r…
vugarsafarzada May 5, 2026
9fa3800
fix: replace regex-based sanitization with sanitize-html library for …
vugarsafarzada May 5, 2026
2826d66
feat: Team management part1, add DataTable component and migrate serv…
vugarsafarzada May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,25 @@
"dependencies": {
"@base-ui/react": "^1.3.0",
"@reduxjs/toolkit": "^2.11.2",
"@tiptap/extension-color": "^3.22.5",
"@tiptap/extension-placeholder": "^3.22.5",
"@tiptap/extension-text-style": "^3.22.5",
"@tiptap/extension-underline": "^3.22.5",
"@tiptap/pm": "^3.22.5",
"@tiptap/react": "^3.22.5",
"@tiptap/starter-kit": "^3.22.5",
"@types/sanitize-html": "^2.16.1",
"axios": "1.14.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^1.7.0",
"next": "16.2.1",
"react": "19.2.4",
"react-day-picker": "^9.14.0",
"react-dom": "19.2.4",
"react-redux": "^9.2.0",
"sanitize-html": "^2.17.3",
"shadcn": "^4.2.0",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
Expand Down
629 changes: 629 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

33 changes: 30 additions & 3 deletions src/app/(protected)/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { UserProfilePanel } from "@/components/organisms";
import { getMessages } from "@/i18n/config";
import { getServerLocale } from "@/i18n/server";
import { fetchAccountBrands } from "@/lib/brands-api";
import { buildPageTitle } from "@/lib/page-metadata";
import { requireProtectedRouteAccess } from "@/lib/protected-route";
import { fetchPublicServices } from "@/lib/services-api";
import { fetchUserProfileById } from "@/lib/users-api";

type AccountPageProps = {
Expand All @@ -22,9 +24,28 @@ function getSearchParamValue(
return value ?? null;
}

export async function generateMetadata(): Promise<Metadata> {
export async function generateMetadata({
searchParams,
}: AccountPageProps): Promise<Metadata> {
const locale = await getServerLocale();
const messages = getMessages(locale);
const resolvedSearchParams = (await searchParams) ?? {};
const requestedUserId = getSearchParamValue(resolvedSearchParams.id)?.trim();

if (requestedUserId) {
const cookieStore = await cookies();
const accessToken = cookieStore.get("rzp_at")?.value;
const targetUser = accessToken
? await fetchUserProfileById(requestedUserId, accessToken).catch(() => null)
: null;
const fullName = targetUser
? `${targetUser.first_name} ${targetUser.last_name}`.trim()
: null;

return {
title: buildPageTitle(messages.dashboard.profile, fullName),
};
}

return {
title: messages.dashboard.account,
Expand Down Expand Up @@ -53,7 +74,13 @@ export default async function AccountPage({ searchParams }: AccountPageProps) {
notFound();
}

const brands = await fetchAccountBrands(requestedUserId, accessToken).catch(() => []);
const [brands, services] = await Promise.all([
fetchAccountBrands(requestedUserId, accessToken).catch(() => []),
fetchPublicServices(
{ owner_id: requestedUserId, direct_only: true },
accessToken,
).catch(() => []),
]);

return <UserProfilePanel user={targetUser} brands={brands} />;
return <UserProfilePanel user={targetUser} brands={brands} services={services} />;
}
172 changes: 167 additions & 5 deletions src/app/(protected)/brands/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { AccountBrandsSection } from "@/components/organisms/account-brands-section/account-brands-section";
import { requireProtectedRouteAccess } from "@/lib/protected-route";
import {
fetchMyBrands,
fetchBrandById,
fetchActiveBrands,
fetchActiveBrandsPage,
fetchAccountBrands,
fetchBrandCategories,
fetchBrandTeamWorkspace,
} from "@/lib/brands-api";
import { fetchPublicServices } from "@/lib/services-api";
import { fetchMarketplaceFacets } from "@/lib/marketplace-api";
import { fetchBrandForReview } from "@/lib/moderation-api";
import { BrandsUsoPage } from "@/components/organisms/brands-uso-page";
import { BrandsUcrPage } from "@/components/organisms/brands-ucr-page";
import { BrandDetail } from "@/components/organisms/brand-detail";
Expand All @@ -18,7 +22,9 @@ import { BrandTeamWorkspace } from "@/components/organisms/brand-team-workspace"
import { fetchUserProfileById } from "@/lib/users-api";
import { getMessages } from "@/i18n/config";
import { getServerLocale } from "@/i18n/server";
import type { Brand, PublicUserProfile } from "@/types";
import { buildPageTitle } from "@/lib/page-metadata";
import type { Brand, BrandStatus, PublicUserProfile } from "@/types";
import type { ModerationBrandDetail } from "@/types/moderation";

type BrandsPageProps = {
searchParams?: Promise<Record<string, string | string[] | undefined>>;
Expand Down Expand Up @@ -58,6 +64,113 @@ async function fetchBrandOwnersById(
);
}

function mapModerationBrandToBrand(brand: ModerationBrandDetail): Brand {
return {
id: brand.id,
name: brand.name,
description: brand.description ?? undefined,
status: brand.status as BrandStatus,
owner_id: brand.owner.id,
logo_url: brand.logo_url ?? undefined,
gallery: (brand.gallery ?? []).map((item, index) => ({
id: `${brand.id}-gallery-${index}`,
media_id: `${brand.id}-gallery-media-${index}`,
url: item.url,
order: item.order ?? index,
})),
branches: (brand.branches ?? []).map((branch) => ({
id: branch.id,
brand_id: brand.id,
name: branch.name,
description: branch.description ?? undefined,
address1: branch.address1,
address2: branch.address2 ?? undefined,
phone: branch.phone ?? undefined,
email: branch.email ?? undefined,
is_24_7: branch.is_24_7 ?? false,
opening: branch.opening ?? undefined,
closing: branch.closing ?? undefined,
breaks: [],
cover_url: branch.cover_url ?? undefined,
})),
categories: brand.categories ?? [],
rating: null,
rating_count: 0,
my_rating: null,
created_at: brand.created_at,
updated_at: brand.updated_at ?? brand.created_at,
};
}

function mapModerationBrandOwnerToProfile(brand: ModerationBrandDetail): PublicUserProfile {
return {
id: brand.owner.id,
first_name: brand.owner.first_name,
last_name: brand.owner.last_name,
email: brand.owner.email,
type: "uso",
avatar_url: brand.owner.avatar_url ?? null,
created_at: brand.owner.created_at ?? brand.created_at,
updated_at: brand.owner.created_at ?? brand.created_at,
};
}

export async function generateMetadata({
searchParams,
}: BrandsPageProps): Promise<Metadata> {
const [locale, resolvedParams, cookieStore] = await Promise.all([
getServerLocale(),
searchParams ?? Promise.resolve({}),
cookies(),
]);
const messages = getMessages(locale);
const progress = getStringParam(resolvedParams, "progress");
const brandId = getStringParam(resolvedParams, "id");
const accountUserId = getStringParam(resolvedParams, "account");
const accessToken = cookieStore.get("rzp_at")?.value ?? "";

if (accountUserId && !progress && !brandId) {
const targetUser = await fetchUserProfileById(accountUserId, accessToken).catch(() => null);
const fullName = targetUser
? `${targetUser.first_name} ${targetUser.last_name}`.trim()
: null;

return {
title: buildPageTitle(messages.profile.brandsSectionTitle, fullName),
};
}

if (progress === "create") {
return {
title: buildPageTitle(messages.dashboard.brands, messages.brands.createBrand),
};
}

if (brandId) {
const brand = await fetchBrandById(brandId, accessToken).catch(() => null);

if (progress === "edit") {
return {
title: buildPageTitle(messages.brands.editBrand, brand?.name),
};
}

if (progress === "team") {
return {
title: buildPageTitle(messages.brands.teamWorkspace, brand?.name),
};
}

return {
title: buildPageTitle(messages.brands.detailTitle, brand?.name),
};
}

return {
title: messages.dashboard.brands,
};
}

export default async function BrandsPage({ searchParams }: BrandsPageProps) {
const resolvedParams = await (searchParams ?? Promise.resolve({}));
const user = await requireProtectedRouteAccess("/brands", resolvedParams);
Expand Down Expand Up @@ -137,6 +250,26 @@ export default async function BrandsPage({ searchParams }: BrandsPageProps) {

// ── Brand detail view (?id=<brand_id>) ────────────────────────────────────
if (brandId && !progress) {
if (user.type === "admin") {
const moderationBrand = await fetchBrandForReview(brandId, accessToken).catch(() => null);

if (!moderationBrand) {
return (
<div style={{ padding: "2rem", color: "var(--app-text-muted)", fontSize: "var(--font-size-small)" }}>
Brand not found.
</div>
);
}

return (
<BrandDetail
brand={mapModerationBrandToBrand(moderationBrand)}
currentUserId={user.id}
owner={mapModerationBrandOwnerToProfile(moderationBrand)}
/>
);
}

const brand = await fetchBrandById(brandId, accessToken).catch(() => null);

if (!brand) {
Expand Down Expand Up @@ -225,8 +358,37 @@ export default async function BrandsPage({ searchParams }: BrandsPageProps) {

// ── UCR default view (should only land here with ?id, handled above) ───────
// Fallback: show the active brands gallery
const brands = await fetchActiveBrands(accessToken).catch(() => []);
const ownersById = await fetchBrandOwnersById(brands, accessToken);
const activeBrandCategoryId =
getStringParam(resolvedParams, "category") ??
getStringParam(resolvedParams, "brand_category_id");
const [brandsPage, featuredServices, marketplaceFacets] = await Promise.all([
fetchActiveBrandsPage(accessToken, {
page: 1,
limit: 24,
...(activeBrandCategoryId && { brand_category_id: activeBrandCategoryId }),
}).catch(() => ({
brands: [],
meta: { page: 1, limit: 24, total_count: 0, has_more: false },
})),
fetchPublicServices({}, accessToken).catch(() => []),
fetchMarketplaceFacets(accessToken).catch(() => ({
service_categories: [],
brand_categories: [],
})),
]);
const ownersById = await fetchBrandOwnersById(brandsPage.brands, accessToken);
const activeServices = featuredServices.filter((s) => s.status === "ACTIVE");

return <BrandsUcrPage brands={brands} ownersById={ownersById} />;
return (
<BrandsUcrPage
key={activeBrandCategoryId ?? "all"}
brands={brandsPage.brands}
ownersById={ownersById}
featuredServices={activeServices}
accessToken={accessToken}
initialMeta={brandsPage.meta}
brandCategories={marketplaceFacets.brand_categories}
activeBrandCategoryId={activeBrandCategoryId}
/>
);
}
12 changes: 12 additions & 0 deletions src/app/(protected)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import type { Metadata } from "next";
import { ProtectedComingSoonRoute } from "@/components/organisms/protected-coming-soon-route";
import { getMessages } from "@/i18n/config";
import { getServerLocale } from "@/i18n/server";

export async function generateMetadata(): Promise<Metadata> {
const locale = await getServerLocale();
const messages = getMessages(locale);

return {
title: messages.dashboard.dashboardPage,
};
}

export default function DashboardPage() {
return <ProtectedComingSoonRoute path="/dashboard" />;
Expand Down
Loading