feat(ui): update UI v1
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* مختصات پروژههای گروه ساختمانی آرامش — برگرفته از شیت «Sheet1.csv».
|
||||
*
|
||||
* دو نکتهی دادهای که در همانجا حل میشوند:
|
||||
* ۱) ستونهای neshan_url و google_maps_url در شیت فقط برچسب بودند
|
||||
* («Open in Neshan») و نشانی واقعی نداشتند؛ پس پیوندها از روی مختصات
|
||||
* ساخته میشوند که الگوی تثبیتشدهی هر دو سرویس است.
|
||||
* ۲) ستون توضیحات خالی است؛ متن پیشفرض در لایهی رابط کاربری جایگزین میشود.
|
||||
*/
|
||||
|
||||
import type { Project, ProjectCardItem } from './site'
|
||||
|
||||
export type MapProject = {
|
||||
id: number
|
||||
name: string
|
||||
/** سال شمسی — ممکن است خالی باشد */
|
||||
year: string
|
||||
/** پروژهی در جریان (ستون new شیت) */
|
||||
isNew: boolean
|
||||
lat: number
|
||||
lng: number
|
||||
description: string
|
||||
}
|
||||
|
||||
/** مرکز پیشفرض نقشه — اصفهان */
|
||||
export const ISFAHAN_CENTER: [number, number] = [51.6675, 32.6539]
|
||||
|
||||
type CsvRow = { name: string; year: string; isNew: boolean; lat: number; lng: number; description: string }
|
||||
|
||||
/**
|
||||
* ستونهای برچسبی شیت حذف شدهاند (نشانی واقعی نداشتند) و ستون توضیحات به
|
||||
* description نگاشت شده است. ترتیب: name,year,new,lat,lng,description
|
||||
*/
|
||||
const RAW_CSV = `name,year,new,lat,lng,description
|
||||
پروژه سروش,1381,no,32.6662842,51.701735,
|
||||
چهرازی-زیتون,1383,no,32.6700428,51.69985,
|
||||
جعفریان,1384,no,32.6669259,51.701085,
|
||||
بهار,1385,yes,32.6468963,51.6899119,
|
||||
حسن مرادی,1385,no,32.6704278,51.6985567,
|
||||
کوثر,1386,no,32.6522044,51.6856474,
|
||||
صبا,1387,no,32.6541363,51.6847828,
|
||||
پورمعالی - بهشت,1387,no,32.6500402,51.6919823,
|
||||
2 پروژه- فردوس,1388,no,32.6614576,51.706429,
|
||||
ارم,1388,no,32.6563712,51.710694,
|
||||
سینا,1388,no,32.6469299,51.6855403,
|
||||
مسجد سید,1389,yes,32.6669953,51.6557085,
|
||||
هفده شهریور,1389,yes,32.7249998,51.6543819,
|
||||
آرامش رفیع زاده,1390,no,32.6467107,51.6873398,
|
||||
بن بست شریعت,1390,yes,32.6459939,51.6897556,
|
||||
قصر - یاسین,1390,no,32.6663549,51.7065512,
|
||||
پردیس,1390,no,32.6518961,51.6973621,
|
||||
آرامش حکیم,1392,no,32.6426783,51.6813477,
|
||||
آرامش دخانیات,1393,no,32.6411924,51.6797555,
|
||||
بهشتی فرشادی,1393,no,32.6531989,51.6790984,
|
||||
کاکتوس,1394,no,32.6486747,51.6859003,
|
||||
آرامش - صیاد,1395,no,32.6486798,51.712659,
|
||||
کوثر,1395,yes,32.6506204,51.6344053,
|
||||
آرامش - بوعلی 1,1396,no,32.6455156,51.6918424,
|
||||
میرزا نصیر,1396,no,32.668195,51.7173079,
|
||||
نیکان,1396,yes,32.6504663,51.6355982,
|
||||
آرامش-رسا,1397,no,32.6522126,51.6857297,
|
||||
نگین,1397,yes,32.6507084,51.6313395,
|
||||
آرامش - بوعلی 2,1398,no,32.645391,51.6917828,
|
||||
آرامش استاد همایی,1398,no,32.6563505,51.691988,
|
||||
آرامش صدر,1398,no,32.6469234,51.6826176,
|
||||
آرامش - سپهسالار,1399,yes,32.6259658,51.7080268,
|
||||
آرامش- صدر,1399,no,32.6469205,51.6829887,
|
||||
آرامش - سنایی,1400,yes,32.6973339,51.6194128,
|
||||
مغازه لباس فروشی,1400,no,32.6516965,51.7020497,
|
||||
آرامش - آتشگاه,1401,yes,32.6547584,51.6319244,
|
||||
آرامش - بزرگمهر,1401,no,32.651381,51.6993575,
|
||||
آرامش - سعدی,1401,yes,32.6970115,51.6196909,
|
||||
آرامش - صمدیه لباف,1401,yes,32.6716585,51.6359248,
|
||||
آرامش 2,1401,yes,32.6505704,51.6337863,
|
||||
آرامش 5 رمضان,1401,yes,32.6674407,51.6568827,
|
||||
آرامش احمدآباد,1401,no,32.6626101,51.6969828,
|
||||
تجاری آتشگاه,1401,yes,32.6515087,51.6033278,
|
||||
آرامش - شهیدان,1402,yes,32.6854968,51.6171964,
|
||||
آرامش خواجو,1402,no,32.6419187,51.6831873,
|
||||
آرامش- اداری ابوالحسن,1402,no,32.6453749,51.6915519,
|
||||
آرامش - انقلاب,1403,yes,32.719769,51.645921,
|
||||
آرامش - شریف,1404,no,32.6473319,51.6955996,
|
||||
آرامش - کهندژ,1404,yes,32.6649497,51.630077,
|
||||
آرامش جی,1404,no,32.6610717,51.7110184,
|
||||
آرامش - بابک,1405,yes,32.674076,51.6368204,
|
||||
آرامش - شمس آبادی,1405,yes,32.6560844,51.664771,
|
||||
آرامش - میرزاطاهر 3,1405,yes,32.6495532,51.6178643,
|
||||
آرامش - وحدت,1405,yes,32.6537564,51.6197007,
|
||||
آرامش پروین,1405,no,32.6618223,51.7098152,
|
||||
خلیفه سلطانی,1405,yes,32.7035398,51.6223296,
|
||||
آرامش - میرزا طاهر 5,1406,yes,32.6517244,51.6202975,
|
||||
آرامش - میرزاطاهر 6,1408,yes,32.6505366,51.6267342,
|
||||
آرامش - میرزا طاهر 4,,yes,32.6510328,51.631436,
|
||||
آرامش- میرزا طاهر 1,,yes,32.6506236,51.6298238,`
|
||||
|
||||
/**
|
||||
* تجزیهی یک خط CSV — پشتیبانی از فیلدهای گیومهدار و گیومهی دوگانهی درونی.
|
||||
* جداکننده کاما و نویسههای مخفی ابتدای فایل (BOM) هم حذف میشوند.
|
||||
*/
|
||||
function parseCsvLine(line: string): string[] {
|
||||
const fields: string[] = []
|
||||
let current = ''
|
||||
let inQuotes = false
|
||||
|
||||
for (let i = 0; i < line.length; i += 1) {
|
||||
const char = line[i]
|
||||
if (inQuotes) {
|
||||
if (char === '"') {
|
||||
if (line[i + 1] === '"') {
|
||||
current += '"'
|
||||
i += 1
|
||||
} else inQuotes = false
|
||||
} else current += char
|
||||
} else if (char === '"') inQuotes = true
|
||||
else if (char === ',') {
|
||||
fields.push(current.trim())
|
||||
current = ''
|
||||
} else current += char
|
||||
}
|
||||
fields.push(current.trim())
|
||||
return fields
|
||||
}
|
||||
|
||||
/** CSV خام → آرایهی تایپشدهی JSON */
|
||||
export function parseProjectsCsv(raw: string): MapProject[] {
|
||||
const rows: CsvRow[] = []
|
||||
const lines = raw.replace(/^\ufeff/, '').split(/\r?\n/)
|
||||
|
||||
for (const line of lines.slice(1)) {
|
||||
if (!line.trim()) continue
|
||||
const [name, year = '', isNew = '', lat = '', lng = '', description = ''] = parseCsvLine(line)
|
||||
const parsedLat = Number(lat)
|
||||
const parsedLng = Number(lng)
|
||||
if (!name || !Number.isFinite(parsedLat) || !Number.isFinite(parsedLng)) continue
|
||||
rows.push({ name, year, isNew: isNew.toLowerCase() === 'yes', lat: parsedLat, lng: parsedLng, description })
|
||||
}
|
||||
|
||||
return rows.map((row, index) => ({ id: index + 1, ...row }))
|
||||
}
|
||||
|
||||
export const MAP_PROJECTS: MapProject[] = parseProjectsCsv(RAW_CSV)
|
||||
|
||||
/** پیوند نشان روی نقشه برای سرویس نشان */
|
||||
export function buildNeshanUrl(project: Pick<MapProject, 'lat' | 'lng'>, zoom = 17): string {
|
||||
return `https://neshan.org/maps/@${project.lat},${project.lng},${zoom}z`
|
||||
}
|
||||
|
||||
/** پیوند نقشه گوگل */
|
||||
export function buildGoogleMapsUrl(project: Pick<MapProject, 'lat' | 'lng'>): string {
|
||||
return `https://www.google.com/maps?q=${project.lat},${project.lng}`
|
||||
}
|
||||
|
||||
const NAME_NOISE = ['آرامش', 'پروژه', 'مجتمع', 'گروه', 'ساختمانی', 'برج', 'مجموعه']
|
||||
|
||||
/**
|
||||
* نرمالسازی نام برای تطبیق شیت با دادهی بکاند: نویسههای نامرئی،
|
||||
* ی/ک عربی، نشانهها و واژههای عمومی حذف میشوند تا «آرامش - سنایی» و
|
||||
* «پروژه سنایی» یک کلید بگیرند.
|
||||
*/
|
||||
export function normalizeProjectName(name: string): string {
|
||||
let result = (name ?? '')
|
||||
.normalize('NFC')
|
||||
.replace(/[\u200b-\u200f\u202a-\u202e\ufeff]/g, ' ')
|
||||
.replace(/[يى]/g, 'ی')
|
||||
.replace(/ك/g, 'ک')
|
||||
.replace(/[ۀة]/g, 'ه')
|
||||
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
for (const noise of NAME_NOISE) result = result.replace(new RegExp(`(^|\\s)${noise}(?=\\s|$)`, 'g'), ' ')
|
||||
return result.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** پروژهی شیت متناظر با یک نام (تطبیق دقیق نرمالشده، سپس دربرگیری) */
|
||||
export function findMapProjectByName(name: string): MapProject | undefined {
|
||||
const key = normalizeProjectName(name)
|
||||
if (!key) return undefined
|
||||
|
||||
const exact = MAP_PROJECTS.find((project) => normalizeProjectName(project.name) === key)
|
||||
if (exact) return exact
|
||||
|
||||
return MAP_PROJECTS.find((project) => {
|
||||
const candidate = normalizeProjectName(project.name)
|
||||
return candidate.length >= 3 && key.length >= 3 && (candidate.includes(key) || key.includes(candidate))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* محل جایگزین وقتی پروژهای در شیت پیدا نشد — مرکز اصفهان با نام پروژهی
|
||||
* بکاند، تا نمای تکپروژه هرگز بدون نقشه نماند.
|
||||
*/
|
||||
export function createPlaceholderProject(name: string, description = ''): MapProject {
|
||||
return {
|
||||
id: 0,
|
||||
name,
|
||||
year: '',
|
||||
isNew: false,
|
||||
lat: ISFAHAN_CENTER[1],
|
||||
lng: ISFAHAN_CENTER[0],
|
||||
description,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* کادر محدودهٔ نقشه از مختصات پروژهها — با حاشیه تا لبهٔ نقشه خالی نماند و
|
||||
* کاربر به بیرون از محدودهٔ پروژهها جابهجا نشود.
|
||||
*/
|
||||
export function boundsForProjects(
|
||||
projects: Pick<MapProject, 'lat' | 'lng'>[],
|
||||
padding = 0.02,
|
||||
): [[number, number], [number, number]] | undefined {
|
||||
if (!projects.length) return undefined
|
||||
const lats = projects.map((project) => project.lat)
|
||||
const lngs = projects.map((project) => project.lng)
|
||||
return [
|
||||
[Math.min(...lngs) - padding, Math.min(...lats) - padding],
|
||||
[Math.max(...lngs) + padding, Math.max(...lats) + padding],
|
||||
]
|
||||
}
|
||||
|
||||
/** ردیف شیت → کارت فهرست: عنوان از شیت، وضعیت از ستون new، نشانی از مختصات */
|
||||
export function csvProjectToCard(project: MapProject, detailHref: string): ProjectCardItem {
|
||||
return {
|
||||
id: project.id,
|
||||
title: project.name,
|
||||
status: project.isNew ? 'progress' : 'done',
|
||||
detailHref,
|
||||
addressUrl: buildNeshanUrl(project),
|
||||
}
|
||||
}
|
||||
|
||||
/** پروژهٔ بکاند → کارت فهرست؛ نشانی نشان از تطبیق نامی با شیت میآید */
|
||||
export function apiProjectToCard(project: Project, detailHref: string): ProjectCardItem {
|
||||
const matched = findMapProjectByName(project.title)
|
||||
return {
|
||||
id: project.id,
|
||||
title: project.title,
|
||||
status: project.status,
|
||||
detailHref,
|
||||
location: project.location || undefined,
|
||||
image: project.image || undefined,
|
||||
area: project.area || undefined,
|
||||
addressUrl: matched ? buildNeshanUrl(matched) : undefined,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user