Expose portfolio projectUrl and document it for websites.
Wire create/update/list/detail to project_url and sync website OpenAPI, Postman, and AI brief. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
37bce1e9fa
commit
ad6f9dcf1d
@@ -31,7 +31,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M
|
|||||||
### Typical bootstrap sequence
|
### Typical bootstrap sequence
|
||||||
1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`)
|
1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`)
|
||||||
2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
||||||
3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc); each portfolio may include nullable `projectUrl` (external website link). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
||||||
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||||
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||||
- For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`
|
- For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`
|
||||||
|
|||||||
@@ -906,6 +906,7 @@
|
|||||||
],
|
],
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
"description": "Published portfolios (newest / highest sortOrder first). Items include nullable `projectUrl` (external website link).",
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}/tenants/{{domain}}/portfolios?page=1&pageSize=12",
|
"raw": "{{baseUrl}}/tenants/{{domain}}/portfolios?page=1&pageSize=12",
|
||||||
"host": [
|
"host": [
|
||||||
@@ -948,9 +949,18 @@
|
|||||||
"name": "Get published portfolio by slug (website)",
|
"name": "Get published portfolio by slug (website)",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
"description": "Portfolio detail. Includes nullable `projectUrl` (external website link).",
|
||||||
"url": "{{baseUrl}}/tenants/{{domain}}/portfolios/{{portfolioSlug}}"
|
"url": "{{baseUrl}}/tenants/{{domain}}/portfolios/{{portfolioSlug}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Get published portfolio by id (website)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Preferred for /portfolios/{id}/{titleFaSlug} pages. Includes nullable `projectUrl`.",
|
||||||
|
"url": "{{baseUrl}}/tenants/{{domain}}/portfolios/by-id/{{portfolioId}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "List portfolio comments (website)",
|
"name": "List portfolio comments (website)",
|
||||||
"request": {
|
"request": {
|
||||||
|
|||||||
@@ -1512,7 +1512,7 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "{ items, total, page, pageSize }"
|
"description": "{ items, total, page, pageSize }. Each item includes id, title/titleFa/titleEn, slug, abstract, projectUrl (nullable website link), status, categoryId/categoryName, tags, titleImageUrl, gallery, sortOrder, publishedAt, createdAt, updatedAt. Ordered by sortOrder desc, then publishedAt/createdAt desc."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1538,7 +1538,7 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "{ portfolio }"
|
"description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1564,7 +1564,7 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "{ portfolio }"
|
"description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ export class CreatePortfolioDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
abstract?: string;
|
abstract?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
projectUrl?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
mainTextHtml?: string;
|
mainTextHtml?: string;
|
||||||
@@ -146,6 +150,10 @@ export class UpdatePortfolioDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
abstract?: string | null;
|
abstract?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
projectUrl?: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
mainTextHtml?: string | null;
|
mainTextHtml?: string | null;
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export class PortfoliosService {
|
|||||||
slug,
|
slug,
|
||||||
description: dto.abstract?.trim() || null,
|
description: dto.abstract?.trim() || null,
|
||||||
content: this.buildContent(dto.mainTextHtml) as Prisma.InputJsonValue,
|
content: this.buildContent(dto.mainTextHtml) as Prisma.InputJsonValue,
|
||||||
|
project_url: this.normalizeProjectUrl(dto.projectUrl),
|
||||||
status,
|
status,
|
||||||
featured_media_id: featuredMediaId,
|
featured_media_id: featuredMediaId,
|
||||||
sort_order: sortOrder,
|
sort_order: sortOrder,
|
||||||
@@ -300,6 +301,9 @@ export class PortfoliosService {
|
|||||||
if (dto.abstract !== undefined) {
|
if (dto.abstract !== undefined) {
|
||||||
data.description = dto.abstract?.trim() || null;
|
data.description = dto.abstract?.trim() || null;
|
||||||
}
|
}
|
||||||
|
if (dto.projectUrl !== undefined) {
|
||||||
|
data.project_url = this.normalizeProjectUrl(dto.projectUrl);
|
||||||
|
}
|
||||||
if (dto.status !== undefined) {
|
if (dto.status !== undefined) {
|
||||||
data.status = dto.status;
|
data.status = dto.status;
|
||||||
}
|
}
|
||||||
@@ -743,6 +747,7 @@ export class PortfoliosService {
|
|||||||
titleEn: portfolio.title_en?.trim() || null,
|
titleEn: portfolio.title_en?.trim() || null,
|
||||||
slug: portfolio.slug,
|
slug: portfolio.slug,
|
||||||
abstract: portfolio.description ?? '',
|
abstract: portfolio.description ?? '',
|
||||||
|
projectUrl: portfolio.project_url?.trim() || null,
|
||||||
mainTextHtml: (content.html as string | undefined) ?? '',
|
mainTextHtml: (content.html as string | undefined) ?? '',
|
||||||
status: portfolio.status,
|
status: portfolio.status,
|
||||||
categoryId: categoryAssignment?.categoryId.toString() ?? null,
|
categoryId: categoryAssignment?.categoryId.toString() ?? null,
|
||||||
@@ -835,6 +840,11 @@ export class PortfoliosService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeProjectUrl(value?: string | null): string | null {
|
||||||
|
const trimmed = value?.trim() || '';
|
||||||
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
}
|
||||||
|
|
||||||
private async nextSortOrder(businessId: bigint): Promise<number> {
|
private async nextSortOrder(businessId: bigint): Promise<number> {
|
||||||
const result = await this.prisma.portfolios.aggregate({
|
const result = await this.prisma.portfolios.aggregate({
|
||||||
where: { business_id: businessId },
|
where: { business_id: businessId },
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M
|
|||||||
### Typical bootstrap sequence
|
### Typical bootstrap sequence
|
||||||
1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`)
|
1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`)
|
||||||
2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
||||||
3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc); each portfolio may include nullable `projectUrl` (external website link). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
||||||
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||||
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||||
- For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`
|
- For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`
|
||||||
|
|||||||
@@ -906,6 +906,7 @@
|
|||||||
],
|
],
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
"description": "Published portfolios (newest / highest sortOrder first). Items include nullable `projectUrl` (external website link).",
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}/tenants/{{domain}}/portfolios?page=1&pageSize=12",
|
"raw": "{{baseUrl}}/tenants/{{domain}}/portfolios?page=1&pageSize=12",
|
||||||
"host": [
|
"host": [
|
||||||
@@ -948,9 +949,18 @@
|
|||||||
"name": "Get published portfolio by slug (website)",
|
"name": "Get published portfolio by slug (website)",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
"description": "Portfolio detail. Includes nullable `projectUrl` (external website link).",
|
||||||
"url": "{{baseUrl}}/tenants/{{domain}}/portfolios/{{portfolioSlug}}"
|
"url": "{{baseUrl}}/tenants/{{domain}}/portfolios/{{portfolioSlug}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Get published portfolio by id (website)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"description": "Preferred for /portfolios/{id}/{titleFaSlug} pages. Includes nullable `projectUrl`.",
|
||||||
|
"url": "{{baseUrl}}/tenants/{{domain}}/portfolios/by-id/{{portfolioId}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "List portfolio comments (website)",
|
"name": "List portfolio comments (website)",
|
||||||
"request": {
|
"request": {
|
||||||
|
|||||||
@@ -1512,7 +1512,7 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "{ items, total, page, pageSize }"
|
"description": "{ items, total, page, pageSize }. Each item includes id, title/titleFa/titleEn, slug, abstract, projectUrl (nullable website link), status, categoryId/categoryName, tags, titleImageUrl, gallery, sortOrder, publishedAt, createdAt, updatedAt. Ordered by sortOrder desc, then publishedAt/createdAt desc."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1538,7 +1538,7 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "{ portfolio }"
|
"description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1564,7 +1564,7 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "{ portfolio }"
|
"description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user