Add workshop generate-link type, Email DNS for domains, and editor polish.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
6be867ea7a
commit
3cbadb4001
@@ -91,7 +91,7 @@
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
padding-bottom: 28px;
|
||||
padding-bottom: 22px;
|
||||
box-sizing: border-box;
|
||||
direction: ltr;
|
||||
}
|
||||
@@ -112,15 +112,10 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.chartWrapScroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.chartGrid {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 28px;
|
||||
bottom: 22px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -149,8 +144,8 @@
|
||||
}
|
||||
|
||||
.chartMonth {
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
gap: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.group {
|
||||
@@ -167,8 +162,8 @@
|
||||
}
|
||||
|
||||
.chartMonth .group {
|
||||
flex: 1;
|
||||
min-width: 36px;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bars {
|
||||
@@ -177,14 +172,13 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
gap: 1px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.barWrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
@@ -257,6 +251,9 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
@@ -265,11 +262,22 @@
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chartMonth .label {
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.chartMonth .labelText {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
white-space: nowrap;
|
||||
max-height: 68px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.barPrimary {
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { BusinessMessageKey } from '../i18n/messages'
|
||||
import styles from './DailyActivityChart.module.css'
|
||||
|
||||
const CHART_HEIGHT = 180
|
||||
const LABEL_BAND_DAY = 22
|
||||
const LABEL_BAND_MONTH = 80
|
||||
|
||||
interface DailyActivityChartProps {
|
||||
titleKey: BusinessMessageKey
|
||||
@@ -143,6 +145,7 @@ export function DailyActivityChart({
|
||||
|
||||
const yAxisTicks = useMemo(() => buildYAxisTicks(maxValue), [maxValue])
|
||||
const chartScaleMax = yAxisTicks[yAxisTicks.length - 1] ?? 1
|
||||
const labelBand = granularity === 'month' ? LABEL_BAND_MONTH : LABEL_BAND_DAY
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
if (granularity === 'month') {
|
||||
@@ -191,7 +194,7 @@ export function DailyActivityChart({
|
||||
<div
|
||||
className={styles.yAxis}
|
||||
aria-hidden="true"
|
||||
style={{ height: CHART_HEIGHT + 28 }}
|
||||
style={{ height: CHART_HEIGHT + labelBand, paddingBottom: labelBand }}
|
||||
>
|
||||
{[...yAxisTicks].reverse().map((tick) => (
|
||||
<span key={tick} className={styles.yTick}>
|
||||
@@ -200,17 +203,10 @@ export function DailyActivityChart({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={[
|
||||
styles.chartWrap,
|
||||
granularity === 'month' ? styles.chartWrapScroll : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={styles.chartWrap}>
|
||||
<div
|
||||
className={styles.chartGrid}
|
||||
style={{ height: CHART_HEIGHT }}
|
||||
style={{ height: CHART_HEIGHT, bottom: labelBand }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{yAxisTicks.map((tick) => (
|
||||
@@ -229,7 +225,7 @@ export function DailyActivityChart({
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{ height: CHART_HEIGHT + 28 }}
|
||||
style={{ height: CHART_HEIGHT + labelBand }}
|
||||
role="img"
|
||||
aria-label={t(titleKey)}
|
||||
>
|
||||
@@ -277,9 +273,9 @@ export function DailyActivityChart({
|
||||
<span
|
||||
className={styles.label}
|
||||
lang={locale === 'fa' ? 'fa' : 'en'}
|
||||
dir="ltr"
|
||||
dir={locale === 'fa' ? 'rtl' : 'ltr'}
|
||||
>
|
||||
{item.label}
|
||||
<span className={styles.labelText}>{item.label}</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { listBlogCategories } from '../services/blogCategoryService'
|
||||
import { listPortfolioCategories } from '../services/portfolioCategoryService'
|
||||
import { listProductCategories, mapProductCategoryToUi } from '../services/productCategoryService'
|
||||
import { listVideoCategories } from '../services/videoCategoryService'
|
||||
import { listWorkshopCategories } from '../services/workshopCategoryService'
|
||||
import type { ProductCategory } from '../types/category'
|
||||
import {
|
||||
WEBSITE_LINK_KINDS,
|
||||
@@ -31,6 +32,7 @@ const KIND_LABEL: Record<WebsiteLinkKind, BusinessMessageKey> = {
|
||||
blog: 'website.linkGenerator.types.blog',
|
||||
portfolio: 'website.linkGenerator.types.portfolio',
|
||||
video: 'website.linkGenerator.types.video',
|
||||
workshop: 'website.linkGenerator.types.workshop',
|
||||
}
|
||||
|
||||
const ANIMATION_MS = 220
|
||||
@@ -54,6 +56,8 @@ async function loadCategories(kind: WebsiteLinkKind, signal: AbortSignal) {
|
||||
return listPortfolioCategories(signal)
|
||||
case 'video':
|
||||
return listVideoCategories(signal)
|
||||
case 'workshop':
|
||||
return listWorkshopCategories(signal)
|
||||
case 'product':
|
||||
case 'storeItem':
|
||||
return listProductCategories(signal)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
border: 2px dashed rgba(148, 163, 184, 0.4);
|
||||
border: 2px dashed var(--primary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border: 2px dashed rgba(148, 163, 184, 0.4);
|
||||
border: 2px dashed var(--primary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -41,6 +41,14 @@ export function InstructionCard({
|
||||
const hasAbstract = Boolean(video.abstract?.trim())
|
||||
const titleDir = isFa ? 'rtl' : titleLocale.dir
|
||||
const abstractDir = isFa ? 'rtl' : abstractLocale.dir
|
||||
const hasVideo = Boolean(video.embedCode?.trim())
|
||||
const providerLabel = !hasVideo
|
||||
? null
|
||||
: video.videoProvider === 'youtube'
|
||||
? 'YouTube'
|
||||
: video.videoProvider === 'aparat'
|
||||
? 'Aparat'
|
||||
: null
|
||||
|
||||
return (
|
||||
<article className={cardStyles.card}>
|
||||
@@ -58,9 +66,11 @@ export function InstructionCard({
|
||||
<Play size={28} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
<span className={`${cardStyles.statusBadge} ${cardStyles.verifiedBadge}`}>
|
||||
{video.videoProvider === 'youtube' ? 'YouTube' : 'Aparat'}
|
||||
</span>
|
||||
{providerLabel ? (
|
||||
<span className={`${cardStyles.statusBadge} ${cardStyles.verifiedBadge}`}>
|
||||
{providerLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={cardStyles.body} dir={isFa ? 'rtl' : 'ltr'}>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
padding-bottom: 32px;
|
||||
padding-bottom: 80px;
|
||||
box-sizing: border-box;
|
||||
direction: ltr;
|
||||
}
|
||||
@@ -107,14 +107,13 @@
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.chartGrid {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 32px;
|
||||
bottom: 80px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -132,16 +131,16 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
gap: 14px;
|
||||
min-width: 100%;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding-bottom: 4px;
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
.group {
|
||||
flex: 1;
|
||||
min-width: 36px;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -151,13 +150,16 @@
|
||||
.bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.barWrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -228,13 +230,30 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 68px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-ui);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.labelText {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
white-space: nowrap;
|
||||
max-height: 68px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { listAllProducts } from '../services/productService'
|
||||
@@ -8,7 +8,8 @@ import { useT } from '../i18n/useT'
|
||||
import styles from './ProductActivityChart.module.css'
|
||||
|
||||
const CHART_HEIGHT = 200
|
||||
const BAR_GAP = 6
|
||||
const LABEL_BAND = 80
|
||||
const BAR_GAP = 4
|
||||
|
||||
export function ProductActivityChart() {
|
||||
const t = useT()
|
||||
@@ -16,7 +17,6 @@ export function ProductActivityChart() {
|
||||
const [data, setData] = useState<ProductMonthActivity[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const chartWrapRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
@@ -51,13 +51,6 @@ export function ProductActivityChart() {
|
||||
const yAxisTicks = useMemo(() => buildYAxisTicks(maxValue), [maxValue])
|
||||
const chartScaleMax = yAxisTicks[yAxisTicks.length - 1] ?? 1
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || error) return
|
||||
const wrap = chartWrapRef.current
|
||||
if (!wrap) return
|
||||
wrap.scrollLeft = wrap.scrollWidth
|
||||
}, [isLoading, error, data])
|
||||
|
||||
const totals = useMemo(
|
||||
() => ({
|
||||
added: data.reduce((sum, item) => sum + item.added, 0),
|
||||
@@ -96,7 +89,7 @@ export function ProductActivityChart() {
|
||||
<div
|
||||
className={styles.yAxis}
|
||||
aria-hidden="true"
|
||||
style={{ height: CHART_HEIGHT + 32 }}
|
||||
style={{ height: CHART_HEIGHT + LABEL_BAND, paddingBottom: LABEL_BAND }}
|
||||
>
|
||||
{[...yAxisTicks].reverse().map((tick) => (
|
||||
<span key={tick} className={styles.yTick}>
|
||||
@@ -105,10 +98,10 @@ export function ProductActivityChart() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartWrap} ref={chartWrapRef}>
|
||||
<div className={styles.chartWrap}>
|
||||
<div
|
||||
className={styles.chartGrid}
|
||||
style={{ height: CHART_HEIGHT }}
|
||||
style={{ height: CHART_HEIGHT, bottom: LABEL_BAND }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{yAxisTicks.map((tick) => (
|
||||
@@ -122,7 +115,7 @@ export function ProductActivityChart() {
|
||||
|
||||
<div
|
||||
className={styles.chart}
|
||||
style={{ height: CHART_HEIGHT + 32 }}
|
||||
style={{ height: CHART_HEIGHT + LABEL_BAND }}
|
||||
role="img"
|
||||
aria-label={t('products.activity.chartAria')}
|
||||
>
|
||||
@@ -158,8 +151,12 @@ export function ProductActivityChart() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={styles.label} dir="ltr">
|
||||
{item.label}
|
||||
<span
|
||||
className={styles.label}
|
||||
lang={locale === 'fa' ? 'fa' : 'en'}
|
||||
dir={locale === 'fa' ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<span className={styles.labelText}>{item.label}</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1701,6 +1701,7 @@ const en = {
|
||||
'website.linkGenerator.types.blog': 'Blog',
|
||||
'website.linkGenerator.types.portfolio': 'Portfolio',
|
||||
'website.linkGenerator.types.video': 'Video',
|
||||
'website.linkGenerator.types.workshop': 'Workshop',
|
||||
|
||||
'website.staticImages.subtitle':
|
||||
'Websites load these by key from GET /tenants/{domain}/website/static-images',
|
||||
@@ -4031,6 +4032,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.linkGenerator.types.blog': 'بلاگ',
|
||||
'website.linkGenerator.types.portfolio': 'نمونه کار',
|
||||
'website.linkGenerator.types.video': 'ویدیو',
|
||||
'website.linkGenerator.types.workshop': 'کارگاه',
|
||||
|
||||
'website.staticImages.subtitle':
|
||||
'وبسایتها این تصاویر را با کلید از GET /tenants/{domain}/website/static-images میخوانند',
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
background: transparent;
|
||||
border: 1px solid #0a1628;
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,8 +83,14 @@ export function InstructionDetailsPage() {
|
||||
const contentLocale = textLocaleAttrs(
|
||||
video.descriptionHtml?.replace(/<[^>]+>/g, ' ') ?? '',
|
||||
)
|
||||
const providerLabel =
|
||||
video.videoProvider === 'youtube' ? 'YouTube' : 'Aparat'
|
||||
const hasVideo = Boolean(video.embedCode?.trim())
|
||||
const providerLabel = !hasVideo
|
||||
? null
|
||||
: video.videoProvider === 'youtube'
|
||||
? 'YouTube'
|
||||
: video.videoProvider === 'aparat'
|
||||
? 'Aparat'
|
||||
: null
|
||||
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
|
||||
@@ -167,7 +167,7 @@ export function InstructionListPage() {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className={pageStyles.grid}>
|
||||
<div className={pageStyles.gridCols4}>
|
||||
{videos.map((video) => (
|
||||
<InstructionCard
|
||||
key={video.id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getBusinessDomain } from '../lib/config'
|
||||
|
||||
export type WebsiteLinkKind = 'product' | 'storeItem' | 'blog' | 'portfolio' | 'video'
|
||||
export type WebsiteLinkKind = 'product' | 'storeItem' | 'blog' | 'portfolio' | 'video' | 'workshop'
|
||||
|
||||
export type WebsiteLinkFilter = 'category' | 'tag'
|
||||
|
||||
@@ -10,6 +10,7 @@ export const WEBSITE_LINK_KINDS: WebsiteLinkKind[] = [
|
||||
'blog',
|
||||
'portfolio',
|
||||
'video',
|
||||
'workshop',
|
||||
]
|
||||
|
||||
export const WEBSITE_LINK_KIND_PATH: Record<WebsiteLinkKind, string> = {
|
||||
@@ -18,6 +19,7 @@ export const WEBSITE_LINK_KIND_PATH: Record<WebsiteLinkKind, string> = {
|
||||
blog: '/blogs',
|
||||
portfolio: '/portfolios',
|
||||
video: '/videos',
|
||||
workshop: '/workshops',
|
||||
}
|
||||
|
||||
export function getWebsiteOrigin(domain = getBusinessDomain()): string {
|
||||
|
||||
Reference in New Issue
Block a user