import * as React from 'react' import { map, filter, compose } from '../../util/lazy' import moment = require('moment') import { kebabCase } from 'lodash' const { useCallback, useState } = React const getText = (blob: Blob) => { const fileReader = new FileReader() return new Promise((resolve, reject) => { fileReader.addEventListener('load', () => typeof fileReader.result === 'string' ? resolve(fileReader.result) : reject(new TypeError()) ) fileReader.addEventListener('error', () => reject(fileReader.error)) fileReader.readAsText(blob) }) } const topLevelMapping = { "Destination Url": 'destination_url', "Forward All Query Params": 'forward_all_query_params', "Published?": 'status', } const metadataMapping = { "Audience (Drop Down)": 'audience', "Campaign": 'campaign', "Subcampaign": 'subcampaign', "Channel (Dropdown)": 'channel', "Subchannel": 'subchannel', "Content": 'content', "Medium": 'medium', "Placement (Drop Down)": 'placement', "Source": 'source', } const queryParameterMapping = { "Coupon Code": 'coupon_code', "Plan": 'plan_id', "Upgrade Source": 'upgrade_src', "CX": 'cx', } const knownHeaders = new Set([ ...Object.keys(topLevelMapping), ...Object.keys(metadataMapping), ...Object.keys(queryParameterMapping), ]) type ApiPayload = { slug: string, destination_url: string, forward_all_query_params: boolean, status: string, metadata: Record, additional_query_params: Record } type ApiResult = { trackingLink: { slug: string, destinationUrl: string, forwardAllQueryParams: boolean, status: string, metadata: Record, additionalQueryParams: Record } } export const TrackingLinkBulkUpload = () => { const [isUploading, setIsUploading] = useState(false) const [total, setTotal] = useState(0) const [successful, setSuccessful] = useState(0) const [failed, setFailed] = useState(0) const fileOnChange = useCallback((e: React.ChangeEvent) => { const baseSlug = moment().format('YYYYMMDDHHmmss') getText( e.target.files[0]).then((text) => { const [head, ...body] = text.replace(/\r?\n/g, '\n').split('\n') const headers = head.split(',').map(s => s.trim()) const rows = body.filter(r => r.trim()) setIsUploading(true) setTotal(rows.length) const headerSet = new Set(headers) const missingHeaders = [...filter((s: string) => !headerSet.has(s))(knownHeaders.values())] const unknownHeaders = [...filter((s: string) => !knownHeaders.has(s))(headers.values())] if (missingHeaders.length) { throw new Error(`Incorrect structure: missing headers: ${missingHeaders.join(', ')}`) } if (unknownHeaders.length) { throw new Error(`Incorrect structure: extra headers: ${unknownHeaders.join(', ')}`) } const getData = map((row: string) => row.split(',').reduce((memo, column, idx) => { memo[headers[idx]] = column.trim() return memo }, {})) const mapData = map((row: Record, idx: number): ApiPayload => { const metadata = Object.entries(metadataMapping).reduce((memo, [fromKey, toKey]) => { const value = row[fromKey] if (value) { memo[toKey] = kebabCase(value) } return memo }, {}) const queryParameters = Object.entries(queryParameterMapping) .reduce((memo, [fromKey, toKey]) => { if (toKey !== 'plan_id') { const value = row[fromKey] if (value) { memo[toKey] = kebabCase(value) } } return memo }, {}) const topLevelObj = Object.entries(topLevelMapping).reduce((memo, [fromKey, toKey]) => { if (toKey !== 'forward_all_query_params') { const value = row[fromKey] if (value) { memo[toKey] = value.toLowerCase() } } else { memo[toKey] = row[fromKey].toLowerCase() === 'yes' } return memo }, {}) const slug = `${baseSlug}-${idx}` return { ...topLevelObj, slug, metadata, additional_query_params: queryParameters } as ApiPayload }) const sendData = map((row: ApiPayload) => fetch('/admin/api/tracking_links', { method: 'POST', mode: 'cors', cache: 'no-cache', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ tracking_link: row }) }) .then(res => res.json()) .then(json => { if (Array.isArray(json)) { return Promise.reject(new Error(json.join(' '))) } return json }) ) const markProgress = map((promise: Promise) => { promise .then(() => setSuccessful((s) => s + 1)) .catch(() => setFailed((f) => f + 1)) return promise }) const formatResultColumn = map((response: Promise) => response .then((result) => `https://teachable.com/r/${result.trackingLink.slug}`) .catch((err) => `ERROR: ${err}`) ) const createResultRow = map((promise: Promise, idx: number) => promise .then(col => [rows[idx], col].join(',')) ) const promises = compose( getData, mapData, sendData, markProgress, formatResultColumn, createResultRow )(rows) return Promise.all([[head, 'Teachable URL'].join(','), ...promises]) }).then(rows => { const blob = new Blob([rows.join('\n')], { type: 'text/csv'}) const link = document.createElement('a') const href = URL.createObjectURL(blob) link.href = href link.download = 'links.csv' link.click() URL.revokeObjectURL(href) }) .catch((e) => { alert('An error occurred, check the console for details') console.error(e); }) .finally(() => { setIsUploading(false) setFailed(0) setSuccessful(0) setTotal(0) }) }, []) return ( ) }