base commit

This commit is contained in:
@kirill.komarov
2026-04-30 22:34:55 +05:00
parent 123d86091d
commit 9139a24093
46 changed files with 2023 additions and 153 deletions
+5
View File
@@ -2,3 +2,8 @@
export const apiBaseURL = import.meta.env.VITE_API_URL ?? '/api'
export const STORE_NAME = 'Рукодельная лавка'
/** Демо-контакты для футера; при необходимости задайте через VITE_* в `.env`. */
export const STORE_EMAIL = import.meta.env.VITE_STORE_EMAIL ?? 'hello@example.com'
export const STORE_PHONE = import.meta.env.VITE_STORE_PHONE ?? '+7 (900) 000-00-00'
export const STORE_SOCIAL_NOTE = import.meta.env.VITE_STORE_SOCIAL_NOTE ?? 'Соцсети: укажите ссылки при публикации'
+20 -11
View File
@@ -1,28 +1,37 @@
export const ORDER_STATUSES = [
'DRAFT',
'PENDING_PAYMENT',
'PAYMENT_VERIFICATION',
'PAID',
'IN_PROGRESS',
'SHIPPED',
'READY_FOR_PICKUP',
'DONE',
'CANCELLED',
] as const
export type OrderStatus = (typeof ORDER_STATUSES)[number]
export const ORDER_STATUS_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
DRAFT: ['PENDING_PAYMENT', 'CANCELLED'],
PENDING_PAYMENT: ['PAID', 'CANCELLED'],
PAID: ['IN_PROGRESS', 'CANCELLED'],
IN_PROGRESS: ['SHIPPED', 'CANCELLED'],
SHIPPED: ['DONE'],
DONE: [],
CANCELLED: [],
/** Следующие статусы, доступные админу (смена через PATCH). */
export function getAdminNextOrderStatuses(status: string, deliveryType: 'delivery' | 'pickup'): OrderStatus[] {
switch (status) {
case 'DRAFT':
return ['PENDING_PAYMENT', 'CANCELLED']
case 'PENDING_PAYMENT':
return ['CANCELLED']
case 'PAYMENT_VERIFICATION':
return ['PAID', 'CANCELLED']
case 'PAID':
return ['IN_PROGRESS', 'CANCELLED']
case 'IN_PROGRESS':
if (deliveryType === 'delivery') return ['SHIPPED', 'CANCELLED']
return ['READY_FOR_PICKUP', 'CANCELLED']
default:
return []
}
}
export function canTransitionOrderStatus(from: string, to: string): boolean {
if (from === to) return true
const f = from as OrderStatus
const list = ORDER_STATUS_TRANSITIONS[f]
return Array.isArray(list) ? list.includes(to as OrderStatus) : false
return getAdminNextOrderStatuses(from, 'delivery').includes(to as OrderStatus)
}
+13
View File
@@ -1,13 +1,26 @@
const KEY = 'craftshop_admin_token'
const TOKEN_EVENT = 'craftshop_admin_token_change'
export function getAdminToken(): string | null {
return sessionStorage.getItem(KEY)
}
function notifyTokenListeners(): void {
window.dispatchEvent(new Event(TOKEN_EVENT))
}
/** Подписаться на смену токена (в т. ч. после setAdminToken). */
export function subscribeAdminTokenChange(cb: () => void): () => void {
window.addEventListener(TOKEN_EVENT, cb)
return () => window.removeEventListener(TOKEN_EVENT, cb)
}
export function setAdminToken(token: string): void {
sessionStorage.setItem(KEY, token)
notifyTokenListeners()
}
export function clearAdminToken(): void {
sessionStorage.removeItem(KEY)
notifyTokenListeners()
}
@@ -0,0 +1,7 @@
import { apiBaseURL } from '@/shared/config'
/** Абсолютный или корневой путь начала OAuth на бэкенде (редирект браузера). */
export function oauthAuthorizeUrl(provider: 'vk' | 'yandex'): string {
const base = apiBaseURL.replace(/\/$/, '')
return `${base}/auth/oauth/${provider}`
}
@@ -0,0 +1,15 @@
/** Человекочитаемые подписи к кодам статуса заказа */
export function orderStatusLabelRu(code: string): string {
const map: Record<string, string> = {
DRAFT: 'Черновик',
PENDING_PAYMENT: 'Ожидает оплаты',
PAYMENT_VERIFICATION: 'Проверка оплаты',
PAID: 'Оплачен',
IN_PROGRESS: 'В работе',
SHIPPED: 'Отправлен',
READY_FOR_PICKUP: 'Готово к получению',
DONE: 'Завершён',
CANCELLED: 'Отменён',
}
return map[code] ?? code
}
+12
View File
@@ -0,0 +1,12 @@
/** Склонение «N отзыв(ов…)» для целых n ≥ 0. */
export function reviewsCountRu(n: number): string {
const x = Math.abs(Math.floor(n))
const mod100 = x % 100
const mod10 = x % 10
let word = 'отзывов'
if (mod100 < 11 || mod100 > 14) {
if (mod10 === 1) word = 'отзыв'
else if (mod10 >= 2 && mod10 <= 4) word = 'отзыва'
}
return `${x}\u00a0${word}`
}