Skip to content

Commit 9a42e24

Browse files
committed
feat: new converter
1 parent bce4104 commit 9a42e24

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+4288
-10
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import axios from 'axios'
3+
import { getSID, setCookieHeader } from '../../../../lib/ojs-cookies'
4+
5+
export async function GET(req: NextRequest) {
6+
const { searchParams } = req.nextUrl
7+
const url = searchParams.get('url')
8+
9+
if (!url) return NextResponse.json({ error: 'No url' }, { status: 400 })
10+
11+
try {
12+
const ojssid = req.cookies.get('OJSSID')?.value
13+
const response = await axios.get(url, {
14+
responseType: 'arraybuffer',
15+
headers: {
16+
...(ojssid ? { cookie: `OJSSID=${ojssid}` } : {}),
17+
Accept: 'application/octet-stream,*/*',
18+
},
19+
})
20+
21+
const res = new NextResponse(response.data, {
22+
status: 200,
23+
headers: { 'Content-Type': 'application/octet-stream' },
24+
})
25+
const sid = getSID(response.headers['set-cookie'] || '')
26+
if (sid) {
27+
res.headers.set('Set-Cookie', setCookieHeader('OJSSID', sid, { path: '/' }))
28+
}
29+
return res
30+
} catch (e: any) {
31+
console.error(e?.message || e)
32+
return NextResponse.json({ error: 'Failed to fetch file' }, { status: 500 })
33+
}
34+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import axios from 'axios'
3+
import qs from 'querystring'
4+
import { getSID, setCookieHeader } from '../../../../lib/ojs-cookies'
5+
6+
export async function GET(req: NextRequest) {
7+
const { searchParams } = req.nextUrl
8+
const token = searchParams.get('apiToken')
9+
const endpoint = searchParams.get('endpoint')
10+
const submissionId = searchParams.get('submissionId')
11+
12+
if (!token) return NextResponse.json({ error: 'No token' }, { status: 403 })
13+
if (!endpoint) return NextResponse.json({ error: 'No endpoint' }, { status: 400 })
14+
15+
try {
16+
const decodedEndpoint = decodeURIComponent(endpoint)
17+
const url = `${decodedEndpoint}/submissions/${submissionId}/files?${qs.stringify({
18+
apiToken: token,
19+
})}`
20+
21+
const ojssid = req.cookies.get('OJSSID')?.value
22+
const response = await axios.get(url, {
23+
headers: ojssid ? { cookie: `OJSSID=${ojssid}` } : {},
24+
})
25+
26+
const res = NextResponse.json(response.data)
27+
const sid = getSID(response.headers['set-cookie'] || '')
28+
if (sid) {
29+
res.headers.set('Set-Cookie', setCookieHeader('OJSSID', sid, { path: '/' }))
30+
}
31+
return res
32+
} catch (e: any) {
33+
console.error(e?.message || e)
34+
return NextResponse.json({ error: 'Failed to fetch files' }, { status: 500 })
35+
}
36+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import axios from 'axios'
3+
import { getSID, setCookieHeader } from '../../../../lib/ojs-cookies'
4+
5+
export async function GET(req: NextRequest) {
6+
const { searchParams } = req.nextUrl
7+
const token = searchParams.get('apiToken')
8+
const url = searchParams.get('url')
9+
10+
if (!token) return NextResponse.json({ error: 'No token' }, { status: 403 })
11+
if (!url) return NextResponse.json({ error: 'No url' }, { status: 400 })
12+
13+
try {
14+
const ojssid = req.cookies.get('OJSSID')?.value
15+
const response = await axios.get(`${decodeURIComponent(url)}?apiToken=${token}`, {
16+
headers: ojssid ? { cookie: `OJSSID=${ojssid}` } : {},
17+
})
18+
19+
const res = NextResponse.json(response.data)
20+
const sid = getSID(response.headers['set-cookie'] || '')
21+
if (sid) {
22+
res.headers.set('Set-Cookie', setCookieHeader('OJSSID', sid, { path: '/' }))
23+
}
24+
return res
25+
} catch (e: any) {
26+
console.error(e?.message || e)
27+
return NextResponse.json({ error: 'Failed to fetch publication' }, { status: 500 })
28+
}
29+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import axios from 'axios'
3+
import { getSID, setCookieHeader } from '../../../../lib/ojs-cookies'
4+
5+
export async function GET(req: NextRequest) {
6+
const { searchParams } = req.nextUrl
7+
const token = searchParams.get('apiToken')
8+
const searchPhrase = searchParams.get('searchPhrase')
9+
const endpoint = searchParams.get('endpoint')
10+
11+
if (!token) return NextResponse.json({ error: 'No token' }, { status: 403 })
12+
if (!endpoint) return NextResponse.json({ error: 'No endpoint' }, { status: 400 })
13+
14+
try {
15+
const url = `${endpoint}/submissions?apiToken=${token}${
16+
searchPhrase ? `&searchPhrase=${searchPhrase}` : ''
17+
}`
18+
19+
const ojssid = req.cookies.get('OJSSID')?.value
20+
const response = await axios.get(url, {
21+
headers: ojssid ? { cookie: `OJSSID=${ojssid}` } : {},
22+
})
23+
24+
const res = NextResponse.json(response.data)
25+
const sid = getSID(response.headers['set-cookie'] || '')
26+
if (sid) {
27+
res.headers.set('Set-Cookie', setCookieHeader('OJSSID', sid, { path: '/' }))
28+
}
29+
return res
30+
} catch (e: any) {
31+
console.error(e?.message || e)
32+
return NextResponse.json({ error: 'Failed to fetch submissions' }, { status: 500 })
33+
}
34+
}

apps/converter/app/globals.css

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
@layer base {
6+
:root {
7+
--background: 0 0% 100%;
8+
--foreground: 240 10% 3.9%;
9+
--card: 0 0% 100%;
10+
--card-foreground: 240 10% 3.9%;
11+
--popover: 0 0% 100%;
12+
--popover-foreground: 240 10% 3.9%;
13+
--primary: 240 5.9% 10%;
14+
--primary-foreground: 0 0% 98%;
15+
--secondary: 240 4.8% 95.9%;
16+
--secondary-foreground: 240 5.9% 10%;
17+
--muted: 240 4.8% 95.9%;
18+
--muted-foreground: 240 3.8% 46.1%;
19+
--accent: 240 4.8% 95.9%;
20+
--accent-foreground: 240 5.9% 10%;
21+
--destructive: 0 84.2% 60.2%;
22+
--destructive-foreground: 0 0% 98%;
23+
--border: 240 5.9% 90%;
24+
--input: 240 5.9% 90%;
25+
--ring: 240 5.9% 10%;
26+
--radius: 0.5rem;
27+
}
28+
29+
.dark {
30+
--background: 240 10% 3.9%;
31+
--foreground: 0 0% 98%;
32+
--card: 240 10% 3.9%;
33+
--card-foreground: 0 0% 98%;
34+
--popover: 240 10% 3.9%;
35+
--popover-foreground: 0 0% 98%;
36+
--primary: 0 0% 98%;
37+
--primary-foreground: 240 5.9% 10%;
38+
--secondary: 240 3.7% 15.9%;
39+
--secondary-foreground: 0 0% 98%;
40+
--muted: 240 3.7% 15.9%;
41+
--muted-foreground: 240 5% 64.9%;
42+
--accent: 240 3.7% 15.9%;
43+
--accent-foreground: 0 0% 98%;
44+
--destructive: 0 62.8% 30.6%;
45+
--destructive-foreground: 0 0% 98%;
46+
--border: 240 3.7% 15.9%;
47+
--input: 240 3.7% 15.9%;
48+
--ring: 240 4.9% 83.9%;
49+
}
50+
}
51+
52+
@layer base {
53+
* {
54+
@apply border-border;
55+
}
56+
body {
57+
@apply bg-background text-foreground;
58+
}
59+
}

apps/converter/app/layout.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Metadata } from 'next'
2+
import { Inter } from 'next/font/google'
3+
import './globals.css'
4+
5+
const inter = Inter({ subsets: ['latin'] })
6+
7+
export const metadata: Metadata = {
8+
title: 'JOTE Converter - DOCX to LaTeX',
9+
description: 'Convert DOCX documents to LaTeX with OJS integration - Journal of Trial & Error',
10+
icons: { icon: '/favicon.png' },
11+
}
12+
13+
export default function RootLayout({ children }: { children: React.ReactNode }) {
14+
return (
15+
<html lang="en">
16+
<body className={inter.className}>{children}</body>
17+
</html>
18+
)
19+
}

apps/converter/app/page.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import Image from 'next/image'
5+
import { useCredentialsStore } from '../lib/store'
6+
import { SettingsSheet } from '../components/settings-sheet'
7+
import { CredentialsPrompt } from '../components/credentials-prompt'
8+
import { OjsSearch } from '../components/ojs-search'
9+
import { SubmissionPanel } from '../components/submission-panel'
10+
import { ConversionDropzone } from '../components/conversion-dropzone'
11+
import { LatexOutput } from '../components/latex-output'
12+
import { Separator } from '../components/ui/separator'
13+
14+
export default function Page() {
15+
const { token, endpoint } = useCredentialsStore()
16+
const [skippedSetup, setSkippedSetup] = useState(false)
17+
const hasCredentials = Boolean(token && endpoint)
18+
19+
return (
20+
<div className="min-h-screen">
21+
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
22+
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-4">
23+
<div className="flex items-center gap-3">
24+
<Image src="/favicon.png" alt="JOTE" width={28} height={28} />
25+
<div className="flex items-baseline gap-2">
26+
<h1 className="text-lg font-semibold tracking-tight">JOTE Converter</h1>
27+
<span className="hidden text-xs text-muted-foreground sm:inline">DOCX to LaTeX</span>
28+
</div>
29+
</div>
30+
<SettingsSheet />
31+
</div>
32+
</header>
33+
34+
<main className="mx-auto max-w-5xl space-y-6 px-4 py-8">
35+
{!hasCredentials && !skippedSetup ? (
36+
<CredentialsPrompt onSkip={() => setSkippedSetup(true)} />
37+
) : (
38+
<>
39+
{hasCredentials && (
40+
<>
41+
<section>
42+
<OjsSearch />
43+
</section>
44+
<SubmissionPanel />
45+
<Separator />
46+
</>
47+
)}
48+
49+
<section className="space-y-4">
50+
<h2 className="text-base font-medium">
51+
{hasCredentials ? 'Or upload a file directly' : 'Upload a .docx file'}
52+
</h2>
53+
<ConversionDropzone />
54+
</section>
55+
56+
<LatexOutput />
57+
</>
58+
)}
59+
</main>
60+
61+
<footer className="border-t py-6">
62+
<div className="mx-auto flex max-w-5xl items-center justify-center gap-2 px-4 text-xs text-muted-foreground">
63+
<Image src="/favicon.png" alt="" width={16} height={16} className="opacity-50" />
64+
<span>Center of Trial &amp; Error</span>
65+
</div>
66+
</footer>
67+
</div>
68+
)
69+
}

apps/converter/components.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "tailwind.config.ts",
8+
"css": "app/globals.css",
9+
"baseColor": "zinc",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"aliases": {
14+
"components": "@/components",
15+
"utils": "@/lib/utils",
16+
"ui": "@/components/ui",
17+
"lib": "@/lib"
18+
}
19+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client'
2+
3+
import { useCallback } from 'react'
4+
import { useConversionStore } from '../lib/store'
5+
import { Upload, FileText } from 'lucide-react'
6+
import { Card, CardContent } from './ui/card'
7+
8+
export function ConversionDropzone() {
9+
const { input, setInput, clearInput } = useConversionStore()
10+
11+
const handleDrop = useCallback(
12+
(e: React.DragEvent<HTMLDivElement>) => {
13+
e.preventDefault()
14+
const file = e.dataTransfer.files[0]
15+
if (!file) return
16+
file.arrayBuffer().then(setInput)
17+
},
18+
[setInput],
19+
)
20+
21+
const handleFileSelect = useCallback(
22+
(e: React.ChangeEvent<HTMLInputElement>) => {
23+
const file = e.target.files?.[0]
24+
if (!file) return
25+
file.arrayBuffer().then(setInput)
26+
},
27+
[setInput],
28+
)
29+
30+
if (input) {
31+
return (
32+
<Card>
33+
<CardContent className="flex items-center justify-between py-4">
34+
<div className="flex items-center gap-2">
35+
<FileText className="h-5 w-5 text-muted-foreground" />
36+
<span className="text-sm">Document loaded ({(input.byteLength / 1024).toFixed(1)} KB)</span>
37+
</div>
38+
<button onClick={clearInput} className="text-sm text-muted-foreground hover:text-foreground">
39+
Clear
40+
</button>
41+
</CardContent>
42+
</Card>
43+
)
44+
}
45+
46+
return (
47+
<Card>
48+
<CardContent className="p-0">
49+
<div
50+
onDrop={handleDrop}
51+
onDragOver={(e) => e.preventDefault()}
52+
className="flex cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-muted-foreground/25 px-6 py-12 transition-colors hover:border-muted-foreground/50"
53+
>
54+
<Upload className="h-10 w-10 text-muted-foreground/50" />
55+
<div className="text-center">
56+
<p className="text-sm font-medium">Drop a .docx file here or click to browse</p>
57+
<p className="mt-1 text-xs text-muted-foreground">
58+
The file will be converted to LaTeX on the client side
59+
</p>
60+
</div>
61+
<label className="cursor-pointer rounded-md bg-secondary px-4 py-2 text-sm font-medium hover:bg-secondary/80">
62+
Choose file
63+
<input type="file" accept=".docx" onChange={handleFileSelect} className="hidden" />
64+
</label>
65+
</div>
66+
</CardContent>
67+
</Card>
68+
)
69+
}

0 commit comments

Comments
 (0)