diff --git a/core b/core new file mode 100644 index 0000000..d64ea7e Binary files /dev/null and b/core differ diff --git a/src/_components/Editor/Editor.tsx b/src/_components/Editor/Editor.tsx index fcd07b7..d408872 100644 --- a/src/_components/Editor/Editor.tsx +++ b/src/_components/Editor/Editor.tsx @@ -75,11 +75,8 @@ interface EditorProps { context: ContentItem; defaultContext: ContentItem | undefined; editorSaveHandler: (arg: string) => Promise; - imageUploadHandler: (image: File, context: ContentItem) => Promise; - imagePreviewHandler: ( - imageSource: string, - context: ContentItem - ) => Promise; + imageUploadHandler: (image: File) => Promise; + imagePreviewHandler: (imageSource: string) => Promise; enabled?: boolean; top: number; editorRef?: React.MutableRefObject; @@ -202,9 +199,9 @@ const Editor = React.memo(function EditorC({ imagePlugin({ disableImageResize: true, imageUploadHandler: (image) => - Promise.resolve(imageUploadHandler(image, context)), + Promise.resolve(imageUploadHandler(image)), imagePreviewHandler: (imageSource) => - Promise.resolve(imagePreviewHandler(imageSource, context)), + Promise.resolve(imagePreviewHandler(imageSource)), }), toolbarPlugin({ toolbarContents: () => ( @@ -226,7 +223,7 @@ const Editor = React.memo(function EditorC({ ), }), ], - [initialMarkdown, imageUploadHandler, context, imagePreviewHandler] + [initialMarkdown, imageUploadHandler, imagePreviewHandler] ); const SaveButton = React.memo(function SaveButton() { @@ -306,12 +303,12 @@ const Editor = React.memo(function EditorC({ setSuccess(true)} + onClose={() => setSuccess(false)} > setSuccess(true)} + onClose={() => setSuccess(false)} severity='info' sx={{ width: '100%' }} > diff --git a/src/_components/Editor/NewContentDialog.tsx b/src/_components/Editor/NewContentDialog.tsx index 12533c5..497ac12 100644 --- a/src/_components/Editor/NewContentDialog.tsx +++ b/src/_components/Editor/NewContentDialog.tsx @@ -129,14 +129,14 @@ export function NewContentDialog({ setError(''); try { await handleDialog({ frontmatter }); - } catch (err: any) { - setError(err.message); - } finally { - setIsLoading(false); setTitle(''); setDocType(''); setParent('None'); setSelectedDropDown(''); + } catch (err: any) { + setError(err.message); + } finally { + setIsLoading(false); } }; diff --git a/src/_components/Editor/lib/functions.ts b/src/_components/Editor/lib/functions.ts index 639925b..e66c72d 100644 --- a/src/_components/Editor/lib/functions.ts +++ b/src/_components/Editor/lib/functions.ts @@ -70,15 +70,16 @@ export async function createFile({ ); logger.info(response); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const error = await response.json(); + logger.error('Editor:createFile:Commit failed:', error); + throw new Error(`${error.error}`); } const result = await response.json(); // console.log('Editor:createFile:Commit successful:', data); return result; } catch (e: any) { - // console.error('Editor:createFile:Error committing file:', e.message); - return null; + throw new Error(`${e.message}`); } } diff --git a/src/_features/Mdx/EditorWrapper.tsx b/src/_features/Mdx/EditorWrapper.tsx index 4bae9aa..e031dd0 100644 --- a/src/_features/Mdx/EditorWrapper.tsx +++ b/src/_features/Mdx/EditorWrapper.tsx @@ -5,6 +5,7 @@ import { Box, LinearProgress } from '@mui/material'; import Container from '@mui/material/Container'; import matter from 'gray-matter'; import { usePathname, useRouter } from 'next/navigation'; +import path from 'path'; import React, { useEffect, useRef, useState } from 'react'; import { Editor, NewBranchDialog, NewContentDialog } from '@/components/Editor'; @@ -150,7 +151,7 @@ export default function EditorWrapper({ message: e.message, }); - throw new Error(`Error creating file: ${e.message}`); + throw new Error(`${e.message}`); } } @@ -202,6 +203,112 @@ export default function EditorWrapper({ router.push(newPathname); }; + const onSave = async (content: string | null) => { + try { + if (!content) { + throw new Error('No content to save'); + } + if (!context.file) { + throw new Error('No file to save'); + } + const normalizedFile = context.file.replace(/^\/+/, ''); + + await createFile({ + owner: context.owner, + repo: context.repo, + branch: context.branch, + file: normalizedFile, + content, + message: 'file updated from Airview', + }); + setMdx(content); + return 'success'; + } catch (error: any) { + logger.error('ContentPage:onSave:error: ', error); + throw new Error(`Error saving file: ${error.message}`); + } + }; + + const imageUploadHandler = async (image: File) => { + logger.debug('imageUploadHandler', context, image); + const formData = new FormData(); + formData.append('image', image); + if (!context.file) { + throw new Error('No file to save'); + } + const file = `${path.dirname(context.file)}/${image.name + .replace(/[^a-zA-Z0-9.]/g, '') + .toLowerCase()}`; + const fileName = image.name.replace(/[^a-zA-Z0-9.]/g, '').toLowerCase(); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onloadend = async function uploadImage() { + const base64Image = reader.result; + if (!base64Image) { + throw new Error('error reading image'); + } + let imageData: string[] = []; + if (base64Image && typeof base64Image === 'string') { + imageData = base64Image.split(','); + } + if (!imageData || !imageData[1]) { + throw new Error('error reading image'); + } + + try { + const url = await createFile({ + owner: context.owner, + repo: context.repo, + branch: context.branch, + file, + content: imageData[1], + message: 'image uploaded from Airview', + }); + if (url) { + resolve(fileName); + } else { + throw new Error('Error uploading image'); + } + } catch (error: any) { + throw new Error(error); + } + }; + + reader.onerror = reject; + + reader.readAsDataURL(image); + }); + }; + + const imagePreviewHandler = async ( + imageSource: string + // context: ContentItem + ) => { + logger.info('imagePreviewHandler', context, imageSource); + if (imageSource.startsWith('http')) return imageSource; + if (!context.file) { + throw new Error('No file context'); + } + const file = `${path.dirname(context.file)}/${imageSource.replace(/^\/|^\.\//, '')}`; + const filePath = file.replace(/^\/|^\.\//, ''); // strip leading slash + logger.debug('imagePreviewHandler', filePath); + + const response = await fetch( + `/api/github/content?owner=${context.owner}&repo=${context.repo}&branch=${context.branch}&path=${filePath}` + ); + if (!response.ok) { + throw new Error(`${response.status}`); + } + // Fetch the image as Blob directly + const blob = await response.blob(); + + // Create an object URL for the Blob + const imageObjectUrl = URL.createObjectURL(blob); + return imageObjectUrl; + }; + return ( <> Promise.resolve('')} + editorSaveHandler={onSave} enabled - imagePreviewHandler={() => Promise.resolve('')} - imageUploadHandler={() => Promise.resolve('')} + imagePreviewHandler={imagePreviewHandler} + imageUploadHandler={imageUploadHandler} markdown={mdx} top={220} /> diff --git a/src/app/api/github/content/route.ts b/src/app/api/github/content/route.ts index cf67857..eef8019 100644 --- a/src/app/api/github/content/route.ts +++ b/src/app/api/github/content/route.ts @@ -83,10 +83,29 @@ export async function POST(req: NextRequest): Promise { { status: 400 } ); } + let content; + let message; - const { content, message } = JSON.parse( - req?.body ? req.body.toString() : '' - ); + try { + // Ensure req.body exists and is not an empty string before parsing + const body = await req.json(); + + if (body) { + content = body.content; + message = body.message; + } + } catch (error) { + logger.error('Error parsing request body as JSON:', error); + return NextResponse.json( + { + error: 'Missing required parameters: content or message in the body', + }, + { status: 400 } + ); + } + // const { content, message } = JSON.parse( + // req?.body ? req.body.toString() : '' + // ); if (!content || !message) { return NextResponse.json( { @@ -103,11 +122,8 @@ export async function POST(req: NextRequest): Promise { content, message ); - NextResponse.json({ response: commitResponse }, { status: 201 }); + return NextResponse.json({ response: commitResponse }, { status: 201 }); } catch (err) { - return NextResponse.json( - { error: `Error in API: ${err}` }, - { status: 500 } - ); + return NextResponse.json({ error: `${err}` }, { status: 500 }); } } diff --git a/src/lib/Github.ts b/src/lib/Github.ts index 3700baa..f2a1fcb 100644 --- a/src/lib/Github.ts +++ b/src/lib/Github.ts @@ -10,7 +10,8 @@ import { cacheRead, cacheWrite } from '@/lib/Redis'; import type { GitHubFile } from '@/lib/Types'; let gitHubInstance: Octokit | undefined; -const logger = getLogger(); +const logger = getLogger().child({ namespace: 'lib/Github' }); + logger.level = 'error'; interface GitHubConfig { @@ -858,7 +859,14 @@ export async function commitFileToBranch( } try { - const branchSha = await getBranchSha(owner, repo, branch); + const { data } = await gitHubInstance.rest.repos.getBranch({ + owner, + repo, + branch, + }); + const branchSha = data.commit.sha; + + // const branchSha = await getBranchSha(owner, repo, branch); const encoding = isBase64(content) ? 'base64' : 'utf-8'; const blob = await gitHubInstance.rest.git.createBlob({ @@ -900,17 +908,20 @@ export async function commitFileToBranch( // refresh branch cache try { // Store the content in the cache before returning it - const cacheKey = `github:getBranch:${owner}:${repo}:${branch}`; + const cacheKey = `github:branch:${owner}:${repo}:${branch}`; + await cacheWrite(cacheKey, newCommit.data.sha, 600); } catch (error) { - logger.error(`[GitHub][getBranchSha] Error writing cache: ${error}`); + logger.error(`[getBranchSha] Error writing cache: ${error}`); } return newCommit.data; - } catch (error) { - logger.error( - `[GitHub][commitFileToBranch] Error committing file: ${error}` - ); + } catch (error: any) { + logger.error(`[commitFileToBranch] Error committing file: ${error as any}`); + const errStr = error.toString(); + if (errStr.includes('Update is not a fast forward')) { + throw new Error('Fast Forward Error (File may already exist)'); + } throw new Error(`${error}`); } }