feat: add bannières page to manage Shopify homepage slideshow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
const API_VERSION = '2024-01'
|
||||
|
||||
function getConfig() {
|
||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||
const token = process.env.SHOPIFY_ADMIN_API_TOKEN
|
||||
if (!domain || !token) throw new Error('SHOPIFY_STORE_DOMAIN et SHOPIFY_ADMIN_API_TOKEN requis')
|
||||
return {
|
||||
baseUrl: `https://${domain}/admin/api/${API_VERSION}`,
|
||||
graphqlUrl: `https://${domain}/admin/api/${API_VERSION}/graphql.json`,
|
||||
headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json' },
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlideSettings {
|
||||
image: string // "shopify://shop_images/filename.jpg"
|
||||
link?: string
|
||||
}
|
||||
|
||||
export interface Slide {
|
||||
blockId: string
|
||||
settings: SlideSettings
|
||||
}
|
||||
|
||||
export interface SlideshowSection {
|
||||
sectionId: string
|
||||
slides: Slide[]
|
||||
blockOrder: string[]
|
||||
sectionSettings: Record<string, unknown>
|
||||
}
|
||||
|
||||
// ── Trouver le thème actif ────────────────────────────────────────────────────
|
||||
|
||||
export async function getActiveThemeId(): Promise<string> {
|
||||
const { baseUrl, headers } = getConfig()
|
||||
const res = await fetch(`${baseUrl}/themes.json`, { headers })
|
||||
const data = await res.json()
|
||||
const main = (data.themes as Array<{ id: number; role: string }>).find(t => t.role === 'main')
|
||||
if (!main) throw new Error('Aucun thème actif trouvé')
|
||||
return String(main.id)
|
||||
}
|
||||
|
||||
// ── Lire le template homepage ─────────────────────────────────────────────────
|
||||
|
||||
export async function getHomepageTemplate(themeId: string): Promise<Record<string, unknown>> {
|
||||
const { baseUrl, headers } = getConfig()
|
||||
const url = `${baseUrl}/themes/${themeId}/assets.json?asset%5Bkey%5D=templates%2Findex.json`
|
||||
const res = await fetch(url, { headers })
|
||||
const data = await res.json()
|
||||
const value = (data.asset as { value: string }).value
|
||||
return JSON.parse(value)
|
||||
}
|
||||
|
||||
// ── Écrire le template homepage ───────────────────────────────────────────────
|
||||
|
||||
export async function saveHomepageTemplate(
|
||||
themeId: string,
|
||||
template: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const { baseUrl, headers } = getConfig()
|
||||
const res = await fetch(`${baseUrl}/themes/${themeId}/assets.json`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({ asset: { key: 'templates/index.json', value: JSON.stringify(template, null, 2) } }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = await res.text()
|
||||
throw new Error(`Shopify theme write error: ${res.status} — ${body}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Extraire le slideshow depuis le template ──────────────────────────────────
|
||||
|
||||
export function extractSlideshow(template: Record<string, unknown>): SlideshowSection | null {
|
||||
const sections = template.sections as Record<string, unknown>
|
||||
if (!sections) return null
|
||||
|
||||
const entry = Object.entries(sections).find(([, v]) => {
|
||||
const sec = v as Record<string, unknown>
|
||||
return typeof sec.type === 'string' && sec.type.includes('slideshow-custom')
|
||||
})
|
||||
if (!entry) return null
|
||||
|
||||
const [sectionId, sec] = entry
|
||||
const section = sec as Record<string, unknown>
|
||||
const blocks = section.blocks as Record<string, { type: string; settings: SlideSettings }>
|
||||
const blockOrder = section.block_order as string[]
|
||||
|
||||
const slides: Slide[] = (blockOrder ?? [])
|
||||
.filter(id => blocks[id]?.type === 'slide')
|
||||
.map(id => ({ blockId: id, settings: blocks[id].settings }))
|
||||
|
||||
return {
|
||||
sectionId,
|
||||
slides,
|
||||
blockOrder: blockOrder ?? [],
|
||||
sectionSettings: (section.settings as Record<string, unknown>) ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mettre à jour les slides dans le template ─────────────────────────────────
|
||||
|
||||
export function applySlideshow(
|
||||
template: Record<string, unknown>,
|
||||
slideshow: SlideshowSection,
|
||||
): Record<string, unknown> {
|
||||
const sections = { ...(template.sections as Record<string, unknown>) }
|
||||
const existing = sections[slideshow.sectionId] as Record<string, unknown>
|
||||
|
||||
const blocks: Record<string, unknown> = {}
|
||||
for (const slide of slideshow.slides) {
|
||||
blocks[slide.blockId] = { type: 'slide', settings: slide.settings }
|
||||
}
|
||||
|
||||
sections[slideshow.sectionId] = {
|
||||
...existing,
|
||||
blocks,
|
||||
block_order: slideshow.slides.map(s => s.blockId),
|
||||
settings: slideshow.sectionSettings,
|
||||
}
|
||||
|
||||
return { ...template, sections }
|
||||
}
|
||||
|
||||
// ── Uploader une image dans Shopify Files et retourner la ref shopify:// ──────
|
||||
|
||||
export async function uploadFileToShopify(
|
||||
imageBase64: string,
|
||||
filename: string,
|
||||
): Promise<string> {
|
||||
const { graphqlUrl, headers } = getConfig()
|
||||
|
||||
// 1. Créer un staged upload
|
||||
const stageRes = await fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
query: `mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
|
||||
stagedUploadsCreate(input: $input) {
|
||||
stagedTargets {
|
||||
url
|
||||
resourceUrl
|
||||
parameters { name value }
|
||||
}
|
||||
userErrors { field message }
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
input: [{
|
||||
filename,
|
||||
mimeType: 'image/jpeg',
|
||||
resource: 'FILE',
|
||||
fileSize: String(Math.ceil(imageBase64.length * 0.75)),
|
||||
httpMethod: 'POST',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
})
|
||||
const stageData = await stageRes.json()
|
||||
const target = stageData?.data?.stagedUploadsCreate?.stagedTargets?.[0]
|
||||
if (!target) throw new Error('Staged upload échoué')
|
||||
|
||||
// 2. Uploader le fichier vers l'URL staging
|
||||
const binaryStr = atob(imageBase64)
|
||||
const bytes = new Uint8Array(binaryStr.length)
|
||||
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
|
||||
const blob = new Blob([bytes], { type: 'image/jpeg' })
|
||||
|
||||
const form = new FormData()
|
||||
for (const { name, value } of target.parameters as Array<{ name: string; value: string }>) {
|
||||
form.append(name, value)
|
||||
}
|
||||
form.append('file', blob, filename)
|
||||
|
||||
const uploadRes = await fetch(target.url, { method: 'POST', body: form })
|
||||
if (!uploadRes.ok) throw new Error(`Upload staging échoué: ${uploadRes.status}`)
|
||||
|
||||
// 3. Enregistrer le fichier dans Shopify Files
|
||||
const createRes = await fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
query: `mutation fileCreate($files: [FileCreateInput!]!) {
|
||||
fileCreate(files: $files) {
|
||||
files {
|
||||
... on MediaImage {
|
||||
id
|
||||
image { url }
|
||||
}
|
||||
}
|
||||
userErrors { field message }
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
files: [{ originalSource: target.resourceUrl, contentType: 'IMAGE' }],
|
||||
},
|
||||
}),
|
||||
})
|
||||
const createData = await createRes.json()
|
||||
const fileUrl = createData?.data?.fileCreate?.files?.[0]?.image?.url as string | undefined
|
||||
|
||||
if (!fileUrl) {
|
||||
// Fallback: construire la ref depuis le resourceUrl
|
||||
const resourceUrl = target.resourceUrl as string
|
||||
const fname = resourceUrl.split('/').pop()?.split('?')[0] ?? filename
|
||||
return `shopify://shop_images/${fname}`
|
||||
}
|
||||
|
||||
// Extraire le nom de fichier depuis l'URL CDN
|
||||
const cdnFilename = fileUrl.split('/files/')[1]?.split('?')[0] ?? filename
|
||||
return `shopify://shop_images/${cdnFilename}`
|
||||
}
|
||||
Reference in New Issue
Block a user