Compare commits
2 commits
dfcb95bd0d
...
bf5686f462
Author | SHA1 | Date | |
---|---|---|---|
bf5686f462 | |||
9fecf5bbfa |
11 changed files with 91 additions and 45 deletions
|
@ -1,3 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
55
Dockerfile
Normal file
55
Dockerfile
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
FROM chimeralinux/chimera:latest AS base
|
||||||
|
RUN apk add libgcc-chimera yarn
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies based on the preferred package manager
|
||||||
|
COPY package.json yarn.lock* ./
|
||||||
|
RUN yarn --frozen-lockfile;
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN yarn run build;
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||||
|
# ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN apk add shadow
|
||||||
|
RUN groupadd --system --gid 1001 nodejs
|
||||||
|
RUN useradd --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 4730
|
||||||
|
|
||||||
|
ENV PORT=4730
|
||||||
|
|
||||||
|
# server.js is created by next build from the standalone output
|
||||||
|
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
CMD ["node", "server.js"]
|
|
@ -1,7 +1,9 @@
|
||||||
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin'
|
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin'
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {}
|
const nextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
}
|
||||||
|
|
||||||
const withVanillaExtract = createVanillaExtractPlugin()
|
const withVanillaExtract = createVanillaExtractPlugin()
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,14 @@ import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
import NetworkError from './types/error/network';
|
import NetworkError from './types/error/network';
|
||||||
import ContentTypeError from './types/error/content_type';
|
import ContentTypeError from './types/error/content_type';
|
||||||
import Image from './types/image';
|
import ImageContent from './types/image';
|
||||||
import Terminal from './types/terminal';
|
import Terminal from './types/terminal';
|
||||||
import Text from './types/text';
|
import Text from './types/text';
|
||||||
|
|
||||||
type ContentType<T> = {
|
type ContentType<T> = {
|
||||||
content_type: RegExp,
|
content_type: RegExp,
|
||||||
extension: RegExp,
|
path?: RegExp,
|
||||||
emit: () => undefined | Processor<T>,
|
emit: () => void | Processor<T>,
|
||||||
};
|
};
|
||||||
type Processor<T> = {
|
type Processor<T> = {
|
||||||
process: (response: Response) => Promise<T>,
|
process: (response: Response) => Promise<T>,
|
||||||
|
@ -22,8 +22,8 @@ function not_match(regex: RegExp | undefined, str: string) {
|
||||||
return !(regex ?? /(?:)/).test(str);
|
return !(regex ?? /(?:)/).test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
function is_type(response: Response, type: ContentType<Any>) {
|
function is_type(response: Response, type: ContentType<any>) {
|
||||||
if (not_match(type.content_type, response.headers.get('Content-Type').split(';')[0])) {
|
if (not_match(type.content_type, response.headers.get('Content-Type')!.split(';')[0])) {
|
||||||
return false;
|
return false;
|
||||||
} else if (not_match(type.path, window.location.pathname)) {
|
} else if (not_match(type.path, window.location.pathname)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -32,12 +32,12 @@ function is_type(response: Response, type: ContentType<Any>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Content({ src }: { src: string}) {
|
export default function Content({ src }: { src: string}) {
|
||||||
const [content, set_content] = useState();
|
const [content, set_content] = useState<JSX.Element>();
|
||||||
|
|
||||||
const recognized_types: ContentType<Any>[] = [{
|
const recognized_types: ContentType<any>[] = [{
|
||||||
content_type: /image\/\w+/,
|
content_type: /image\/\w+/,
|
||||||
emit: () => {
|
emit: () => {
|
||||||
set_content(<Image src={src} />);
|
set_content(<ImageContent src={src} />);
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
content_type: /application\/octet-stream/,
|
content_type: /application\/octet-stream/,
|
||||||
|
@ -70,9 +70,8 @@ export default function Content({ src }: { src: string}) {
|
||||||
if (content == undefined) {
|
if (content == undefined) {
|
||||||
const result = fetch(src)
|
const result = fetch(src)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const content_type = response.headers.get('Content-Type').split(';')[0];
|
|
||||||
for (const type of recognized_types) {
|
for (const type of recognized_types) {
|
||||||
if (type.content_type.test(content_type)) {
|
if (is_type(response, type)) {
|
||||||
const emitted = type.emit();
|
const emitted = type.emit();
|
||||||
if (emitted != undefined) {
|
if (emitted != undefined) {
|
||||||
result.then(emitted.postprocess);
|
result.then(emitted.postprocess);
|
||||||
|
@ -81,13 +80,13 @@ export default function Content({ src }: { src: string}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set_content(<ContentTypeError content_type={content_type} />);
|
set_content(<ContentTypeError content_type={response.headers.get('Content-Type')!.split(';')[0]} />);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
set_content(<NetworkError err={err} />);
|
set_content(<NetworkError err={err} />);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
return content ?? <p>Loading...</p>;
|
return content ?? <p>Loading...</p>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react';
|
// import { useState } from 'react';
|
||||||
|
|
||||||
import * as style from './download_tty.css';
|
import * as style from './download_tty.css';
|
||||||
|
|
||||||
export default function DownloadTTY({ text }: { text: string }) {
|
export default function DownloadTTY({ text }: { text: string }) {
|
||||||
const [copied, set_copied] = useState(false);
|
// const [copied, set_copied] = useState(false);
|
||||||
function make_copy_text(text) {
|
function make_copy_text(text: string) {
|
||||||
return (event) => {
|
return () => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
set_copied(true);
|
// set_copied(true);
|
||||||
// setTimeout(() => { set_copied(false); }, 5000);
|
// setTimeout(() => { set_copied(false); }, 5000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,6 @@ export const download_button = style({
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
marginLeft: '2em',
|
marginLeft: '2em',
|
||||||
border: 0,
|
|
||||||
padding: 0,
|
padding: 0,
|
||||||
border: `1px solid ${colors.background2}`,
|
border: `1px solid ${colors.background2}`,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Metadata, ResolvingMetadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import DownloadTTY from './download_tty';
|
import DownloadTTY from './download_tty';
|
||||||
|
@ -10,7 +10,7 @@ import download_image_light from './download_light.svg';
|
||||||
type SearchParams = { [key: string]: string | string[] | undefined };
|
type SearchParams = { [key: string]: string | string[] | undefined };
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: { file: string },
|
params: { file: string[] },
|
||||||
searchParams: SearchParams,
|
searchParams: SearchParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ function get_root(search_params: SearchParams) {
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
{ params, searchParams }: Props,
|
{ params, searchParams }: Props,
|
||||||
parent: ResolvingMetadata,
|
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
return {
|
return {
|
||||||
title: `${get_path(params.file)} | ${get_root(searchParams)}`,
|
title: `${get_path(params.file)} | ${get_root(searchParams)}`,
|
||||||
|
@ -52,7 +51,7 @@ export default async function Page({
|
||||||
<p className={style.title}>{path}</p>
|
<p className={style.title}>{path}</p>
|
||||||
</div>
|
</div>
|
||||||
<button className={style.download_button}>
|
<button className={style.download_button}>
|
||||||
<a className={style.download_link} href={full} download>
|
<a href={full} download>
|
||||||
<Image
|
<Image
|
||||||
className={style.download_button_image_dark}
|
className={style.download_button_image_dark}
|
||||||
src={download_image_dark}
|
src={download_image_dark}
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import { style } from '@vanilla-extract/css'
|
import { style } from '@vanilla-extract/css'
|
||||||
|
|
||||||
import * as colors from '../../colors.css'
|
|
||||||
|
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
margin: 0,
|
margin: 0,
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
|
|
|
@ -23,19 +23,19 @@ const ansi_bright_colors = [
|
||||||
];
|
];
|
||||||
|
|
||||||
function ansi(text: string) {
|
function ansi(text: string) {
|
||||||
const segments = text.split(/(?=\033)/g);
|
const segments = text.split(/(?=\x1B)/g);
|
||||||
const spans = [];
|
const spans = [];
|
||||||
const style = {
|
const style: {
|
||||||
color: undefined,
|
color?: string,
|
||||||
backgroundColor: undefined,
|
backgroundColor?: string,
|
||||||
fontWeight: undefined,
|
fontWeight?: string,
|
||||||
}
|
} = {};
|
||||||
for (const [index, segment] of segments.entries()) {
|
for (const [index, segment] of segments.entries()) {
|
||||||
const ansi_segment = segment.substring(0, segment.indexOf('m') + 1);
|
const ansi_segment = segment.substring(0, segment.indexOf('m') + 1);
|
||||||
const escape_codes = [
|
const escape_codes = [
|
||||||
...
|
...
|
||||||
ansi_segment.matchAll(/\d+/g)
|
ansi_segment.matchAll(/\d+/g)
|
||||||
].map(element => element[0]);
|
].map(element => Number(element[0]));
|
||||||
if (escape_codes.length == 0) {
|
if (escape_codes.length == 0) {
|
||||||
style.color = undefined;
|
style.color = undefined;
|
||||||
style.backgroundColor = undefined;
|
style.backgroundColor = undefined;
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
import { useLayoutEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import * as style from './text.css';
|
import * as style from './text.css';
|
||||||
|
|
||||||
export default function Text({ text }: { text: string }) {
|
export default function Text({ text }: { text: string }) {
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
const refs = lines.map(() => useRef(null));
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
for (let line of lines) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.group}>
|
<div className={style.group}>
|
||||||
|
@ -22,9 +13,9 @@ export default function Text({ text }: { text: string }) {
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
{lines.map((line, index) => {
|
{lines.map((line, index) => {
|
||||||
if (line != '') {
|
if (line != '') {
|
||||||
return <p ref={refs[index]} key={index} className={style.text}>{line}</p>;
|
return <p key={index} className={style.text}>{line}</p>;
|
||||||
} else {
|
} else {
|
||||||
return <p ref={refs[index]} key={index} className={style.text}>{'\n'}</p>;
|
return <p key={index} className={style.text}>{'\n'}</p>;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"target": "es6",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
Loading…
Reference in a new issue