mirror of
https://github.com/tomru/pdfer.git
synced 2026-03-03 06:27:19 +01:00
move store in local storage
This commit is contained in:
@@ -3,8 +3,10 @@ import LetterOptions from './LetterOptions'
|
|||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import Preview from './Preview'
|
import Preview from './Preview'
|
||||||
import LatestList from './LatestList'
|
import LatestList from './LatestList'
|
||||||
import { generatePdf, getLatest, removeLatest } from './apiHelper'
|
import { generatePdf } from './apiHelper'
|
||||||
import { ILatest } from '../interfaces/ILatest'
|
import { addDocument, removeDocument, getDocuments } from '../lib/dataStore'
|
||||||
|
import { getDocuments as getLatestDocuments } from '../lib/dataStore'
|
||||||
|
import { IStoreItem } from '../interfaces/IStoreItem'
|
||||||
import { IDocProps } from '../interfaces/IDocProps'
|
import { IDocProps } from '../interfaces/IDocProps'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -27,21 +29,13 @@ export default function App() {
|
|||||||
yourRef: '',
|
yourRef: '',
|
||||||
yourRefName: '',
|
yourRefName: '',
|
||||||
})
|
})
|
||||||
const [latest, setLatest] = useState<ILatest[]>([])
|
const [storeData, setStoreData] = useState<IStoreItem[]>([])
|
||||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null)
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null)
|
||||||
const [pdfIsLoading, setPdfIsLoading] = useState<boolean>(false)
|
const [pdfIsLoading, setPdfIsLoading] = useState<boolean>(false)
|
||||||
const [pdfError, setPdfError] = useState<string | null>(null)
|
const [pdfError, setPdfError] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getLatestEffect() {
|
setStoreData(getDocuments())
|
||||||
try {
|
|
||||||
const latest = await getLatest()
|
|
||||||
setLatest(latest)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Unable to get latest:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getLatestEffect()
|
|
||||||
}, [pdfUrl])
|
}, [pdfUrl])
|
||||||
|
|
||||||
const _onChange = (name: string, event: React.ChangeEvent<{ value: string }>) => {
|
const _onChange = (name: string, event: React.ChangeEvent<{ value: string }>) => {
|
||||||
@@ -52,13 +46,13 @@ export default function App() {
|
|||||||
setOptions({ ...options, [name]: value })
|
setOptions({ ...options, [name]: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const _onSelectLatest = (selectedOption: ILatest) => {
|
const _onSelectLatest = (selectedOption: IStoreItem) => {
|
||||||
setOptions({ ...selectedOption })
|
setOptions({ ...selectedOption })
|
||||||
}
|
}
|
||||||
|
|
||||||
const _onRemoveLatest = async (item: ILatest) => {
|
const _onRemoveLatest = async (item: IStoreItem) => {
|
||||||
await removeLatest(item)
|
removeDocument(item.id)
|
||||||
setLatest(latest.filter((curr) => curr.id !== item.id))
|
setStoreData(getLatestDocuments())
|
||||||
}
|
}
|
||||||
|
|
||||||
const _onClear = () => {
|
const _onClear = () => {
|
||||||
@@ -70,7 +64,8 @@ export default function App() {
|
|||||||
setPdfError(null)
|
setPdfError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = await generatePdf(options)
|
const { url, data } = await generatePdf(options)
|
||||||
|
addDocument(data)
|
||||||
setPdfIsLoading(false)
|
setPdfIsLoading(false)
|
||||||
setPdfUrl(url)
|
setPdfUrl(url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -90,7 +85,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<Preview pdfUrl={pdfUrl} pdfIsLoading={pdfIsLoading} pdfError={pdfError} />
|
<Preview pdfUrl={pdfUrl} pdfIsLoading={pdfIsLoading} pdfError={pdfError} />
|
||||||
</div>
|
</div>
|
||||||
<LatestList latest={latest} onSelect={_onSelectLatest} onRemove={_onRemoveLatest} />
|
<LatestList storeData={storeData} onSelect={_onSelectLatest} onRemove={_onRemoveLatest} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ILatest } from '../interfaces/ILatest'
|
import { IStoreItem } from '../interfaces/IStoreItem'
|
||||||
|
|
||||||
export default function LatestList({
|
export default function LatestList({
|
||||||
latest,
|
storeData,
|
||||||
onSelect,
|
onSelect,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
latest: ILatest[]
|
storeData: IStoreItem[]
|
||||||
onSelect: (l: ILatest) => void
|
onSelect: (l: IStoreItem) => void
|
||||||
onRemove: (l: ILatest) => void
|
onRemove: (l: IStoreItem) => void
|
||||||
}) {
|
}) {
|
||||||
if (!latest || !latest.length) {
|
if (!storeData || !storeData.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const latestElements = latest.map((item) => {
|
const latestElements = storeData.map((item) => {
|
||||||
const created = new Date(item.created)
|
const created = new Date(item.created)
|
||||||
const subject = item.subject
|
const subject = item.subject
|
||||||
const hrefId = `#item-${item.id}`
|
const hrefId = `#item-${item.id}`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IDocProps } from '../interfaces/IDocProps'
|
import { IDocProps } from '../interfaces/IDocProps'
|
||||||
import { ILatest } from '../interfaces/ILatest'
|
import { IStoreItem } from '../interfaces/IStoreItem'
|
||||||
|
|
||||||
function checkStatus(res: Response) {
|
function checkStatus(res: Response) {
|
||||||
if (res.status < 200 || res.status >= 400) {
|
if (res.status < 200 || res.status >= 400) {
|
||||||
@@ -7,7 +7,7 @@ function checkStatus(res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generatePdf(state: IDocProps): Promise<string> {
|
export async function generatePdf(state: IDocProps): Promise<{ url: string; data: IStoreItem }> {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -18,18 +18,9 @@ export async function generatePdf(state: IDocProps): Promise<string> {
|
|||||||
|
|
||||||
const response = await fetch('/api/document?template=brief', options)
|
const response = await fetch('/api/document?template=brief', options)
|
||||||
checkStatus(response)
|
checkStatus(response)
|
||||||
const { id } = await response.json()
|
const { id, data } = await response.json()
|
||||||
return `/api/document/${id}`
|
return {
|
||||||
}
|
url: `/api/document/${id}`,
|
||||||
|
data,
|
||||||
export async function getLatest(): Promise<ILatest[]> {
|
}
|
||||||
const response = await fetch('/api/document/latest')
|
|
||||||
checkStatus(response)
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeLatest(item: { id: string }) {
|
|
||||||
const response = await fetch(`/api/document/${item.id}`, { method: 'DELETE' })
|
|
||||||
checkStatus(response)
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IDocProps } from './IDocProps'
|
import { IDocProps } from './IDocProps'
|
||||||
|
|
||||||
export interface ILatest extends IDocProps {
|
export interface IStoreItem extends IDocProps {
|
||||||
id: string
|
id: string
|
||||||
created: Date
|
created: Date
|
||||||
}
|
}
|
||||||
37
lib/dataStore.ts
Normal file
37
lib/dataStore.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { IStoreItem } from '../interfaces/IStoreItem'
|
||||||
|
|
||||||
|
const LOCAL_STORAGE_KEY = 'pdfer-data'
|
||||||
|
|
||||||
|
function readData(): IStoreItem[] {
|
||||||
|
const dataString = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||||
|
|
||||||
|
let data = []
|
||||||
|
|
||||||
|
if (dataString) {
|
||||||
|
data = JSON.parse(dataString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeData(data: IStoreItem[]): void {
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function withoutId(data: IStoreItem[], id: string) {
|
||||||
|
return data.filter((item) => item.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDocument(templateData: IStoreItem) {
|
||||||
|
const data = [templateData, ...withoutId(readData(), templateData.id)]
|
||||||
|
storeData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDocument(id: string) {
|
||||||
|
const newData = withoutId(readData(), id)
|
||||||
|
storeData(newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocuments() {
|
||||||
|
return readData()
|
||||||
|
}
|
||||||
8
lib/id.ts
Normal file
8
lib/id.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createHash } from 'crypto'
|
||||||
|
import { IDocProps } from '../interfaces/IDocProps'
|
||||||
|
|
||||||
|
export function getId(data: IDocProps) {
|
||||||
|
let sum = createHash('sha1')
|
||||||
|
sum.update(JSON.stringify(data))
|
||||||
|
return sum.digest('hex')
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { promises } from 'fs'
|
import { promises } from 'fs'
|
||||||
import { exec } from 'child_process'
|
import { exec } from 'child_process'
|
||||||
import { v1 as uuidv1 } from 'uuid'
|
|
||||||
import { getDirPath, getDocPath, getTexCmd } from './utils'
|
import { getDirPath, getDocPath, getTexCmd } from './utils'
|
||||||
|
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
@@ -10,7 +9,12 @@ async function copyToTemp(id: string, texDocument: string): Promise<void> {
|
|||||||
const dirPath = getDirPath(id)
|
const dirPath = getDirPath(id)
|
||||||
const docPath = getDocPath(id)
|
const docPath = getDocPath(id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promises.access(dirPath)
|
||||||
|
} catch (_) {
|
||||||
|
// well, seems it does not exists, let's create it
|
||||||
await promises.mkdir(dirPath)
|
await promises.mkdir(dirPath)
|
||||||
|
}
|
||||||
await promises.writeFile(docPath, texDocument)
|
await promises.writeFile(docPath, texDocument)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,8 +31,7 @@ async function generateDoc(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function (texDocument: string) {
|
export default async function (id: string, texDocument: string) {
|
||||||
const id = uuidv1()
|
|
||||||
await copyToTemp(id, texDocument)
|
await copyToTemp(id, texDocument)
|
||||||
await generateDoc(id)
|
await generateDoc(id)
|
||||||
return id
|
return id
|
||||||
|
|||||||
15
lib/store.ts
15
lib/store.ts
@@ -1,15 +0,0 @@
|
|||||||
const storeDir = process.env.JSON_STORE || './pdfer-store/'
|
|
||||||
|
|
||||||
import { promisify } from 'util'
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import JsonStore from 'json-fs-store'
|
|
||||||
|
|
||||||
const store = JsonStore(storeDir)
|
|
||||||
|
|
||||||
console.log(`using json-store at ${storeDir}`)
|
|
||||||
|
|
||||||
export const list = promisify(store.list)
|
|
||||||
export const load = promisify(store.load)
|
|
||||||
export const add = promisify(store.add)
|
|
||||||
export const remove = promisify(store.remove)
|
|
||||||
5391
package-lock.json
generated
5391
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -7,27 +7,30 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"type-check": "tsc",
|
"type-check": "tsc",
|
||||||
"format": "prettier --write **/*.{ts,tsx,json}",
|
"format": "prettier --write **/*.{ts,tsx,json}",
|
||||||
|
"test": "jest --watch",
|
||||||
|
"test:ci": "jest --ci",
|
||||||
"docker:build": "docker build . -t pdfer",
|
"docker:build": "docker build . -t pdfer",
|
||||||
"docker:run": "docker run -v ./pdfer-store:/pdfer-store -p 3000:3000 pdfer"
|
"docker:run": "docker run -v /tmp/pdfer-store:/pdfer-store -p 3000:3000 pdfer"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/forms": "^0.2.1",
|
|
||||||
"@types/jest": "^26.0.20",
|
|
||||||
"autoprefixer": "^10.2.4",
|
|
||||||
"json-fs-store": "^1.0.1",
|
|
||||||
"next": "^10.0.7",
|
"next": "^10.0.7",
|
||||||
"postcss": "^8.2.6",
|
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-collapsible": "^2.8.3",
|
"react-dom": "^17.0.1"
|
||||||
"react-dom": "^17.0.1",
|
|
||||||
"tailwindcss": "^2.0.3",
|
|
||||||
"uuid": "^8.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.2.1",
|
||||||
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^14.14.31",
|
"@types/node": "^14.14.31",
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^17.0.1",
|
"@types/react-dom": "^17.0.1",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
|
"autoprefixer": "^10.2.4",
|
||||||
|
"babel-jest": "^26.6.3",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"jest-watch-typeahead": "^0.6.1",
|
||||||
|
"postcss": "^8.2.6",
|
||||||
|
"react-collapsible": "^2.8.3",
|
||||||
|
"tailwindcss": "^2.0.3",
|
||||||
"typescript": "^4.2.2"
|
"typescript": "^4.2.2"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { promises } from 'fs'
|
import { promises } from 'fs'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
import { remove as storeRemove } from '../../../lib/store'
|
|
||||||
import { getPdfPath } from '../../../lib/utils'
|
import { getPdfPath } from '../../../lib/utils'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
@@ -24,22 +23,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.status(404).json({ statusCode: 404, message: 'Method Not Allowed' })
|
res.status(404).json({ statusCode: 404, message: 'Method Not Allowed' })
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'DELETE':
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
query: { id: idArg },
|
|
||||||
} = req
|
|
||||||
|
|
||||||
storeRemove(idArg)
|
|
||||||
|
|
||||||
res.status(202).end()
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
res.status(404).json({ statusCode: 404, message: 'Method Not Allowed' })
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
res.setHeader('Allow', ['GET', 'DELETE'])
|
res.setHeader('Allow', ['GET'])
|
||||||
res.status(405).end(`Method ${method} Not Allowed`)
|
res.status(405).end(`Method ${method} Not Allowed`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
|
|
||||||
import { brief as briefTemplate } from '../../../lib/templates'
|
import { brief as briefTemplate } from '../../../lib/templates'
|
||||||
import renderer from '../../../lib/renderer'
|
import renderer from '../../../lib/renderer'
|
||||||
import * as store from '../../../lib/store'
|
|
||||||
import { IDocProps } from '../../../interfaces/IDocProps'
|
import { IDocProps } from '../../../interfaces/IDocProps'
|
||||||
|
import { getId } from '../../../lib/id'
|
||||||
|
|
||||||
const TEMPLATES: { [key: string]: (options: IDocProps) => string } = {
|
const TEMPLATES: { [key: string]: (options: IDocProps) => string } = {
|
||||||
brief: briefTemplate,
|
brief: briefTemplate,
|
||||||
@@ -28,15 +28,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const id = getId(templateData)
|
||||||
const texDoc = template(templateData)
|
const texDoc = template(templateData)
|
||||||
const id = await renderer(texDoc)
|
await renderer(id, texDoc)
|
||||||
const storeData = Object.assign({}, templateData, {
|
const storeData = { ...templateData, id, created: new Date().toISOString(), template: templateName }
|
||||||
id,
|
res.status(200).json({ id: id, data: storeData })
|
||||||
created: new Date().toISOString(),
|
|
||||||
template: templateName,
|
|
||||||
})
|
|
||||||
await store.add(storeData)
|
|
||||||
res.status(200).json({ id: id })
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error:', err, 'for', req.url)
|
console.error('Error:', err, 'for', req.url)
|
||||||
res.status(500).json({ error: err.toString() })
|
res.status(500).json({ error: err.toString() })
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
import { list as storeList } from '../../../lib/store'
|
|
||||||
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
if (req.method !== 'GET') {
|
|
||||||
res.status(405).json({ statusCode: 405, message: 'Method Not Allowed' })
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const latest = await storeList();
|
|
||||||
res.status(200).json(latest);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error:', err, 'for', req.url);
|
|
||||||
res.status(500).json({ error: err.toString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default handler
|
|
||||||
Reference in New Issue
Block a user