Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
NEXT_PUBLIC_SITE_NAME = 开放黑客松
NEXT_PUBLIC_SITE_SUMMARY = 基于 Git 云开发环境的开放黑客马拉松平台
NEXT_PUBLIC_API_HOST = https://openhackathon-service-server.onrender.com
NEXT_PUBLIC_API_HOST = https://openhackathon-service.onrender.com
NEXT_PUBLIC_AUTHING_APP_ID = 60178760106d5f26cb267ac1
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Open-source [Hackathon][1] Platform with **Git-based Cloud Development Environme
- testing: https://test.hackathon.kaiyuanshe.cn/
- production: https://hackathon.kaiyuanshe.cn/
- RESTful API
- production: https://openhackathon-service-server.onrender.com/
- production: https://openhackathon-service.onrender.com/

## Technology stack

Expand Down
61 changes: 37 additions & 24 deletions components/Activity/ActivityEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Hackathon } from '@kaiyuanshe/openhackathon-service';
import { Hackathon, Media } from '@kaiyuanshe/openhackathon-service';
import { Loading } from 'idea-react';
import { computed } from 'mobx';
import { textJoin } from 'mobx-i18n';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';
import { BadgeInput, Field, FileUploader, RestForm } from 'mobx-restful-table';
import {
ArrayField,
ArrayFieldProps,
Field,
FileUploader,
FormField,
RestForm,
} from 'mobx-restful-table';

import activityStore from '../../models/Activity';
import fileStore from '../../models/Base/File';
Expand Down Expand Up @@ -33,13 +40,14 @@ export class ActivityEditor extends ObservedComponent<ActivityEditorProps, typeo

@computed
get fields(): Field<Hackathon>[] {
const { t } = this.observedContext;
const i18n = this.observedContext;
const { t } = i18n;

return [
{
key: 'name',
renderLabel: t('activity_id'),
pattern: '[a-zA-Z0-9]+',
pattern: '[\\w-]+',
required: true,
invalidMessage: t('name_placeholder'),
},
Expand All @@ -49,31 +57,14 @@ export class ActivityEditor extends ObservedComponent<ActivityEditorProps, typeo
required: true,
invalidMessage: textJoin(t('please_enter'), t('activity_name')),
},
{
key: 'tags',
renderLabel: t('tag'),
renderInput: ({ tags }, { key, ...meta }) => (
<RestForm.FieldBox name={key} {...meta}>
<BadgeInput name={key} placeholder={t('tag_placeholder')} defaultValue={tags} />
</RestForm.FieldBox>
),
},
{ key: 'tags', renderLabel: t('tag'), multiple: true, placeholder: t('tag_placeholder') },
{
key: 'banners',
renderLabel: t('bannerUrls'),
accept: 'image/*',
required: true,
multiple: true,
max: 10,
uploader: fileStore,
renderInput: ({ banners }, { key, uploader, ...meta }) => (
renderInput: ({ banners }, { key, ...meta }) => (
<RestForm.FieldBox name={key} {...meta}>
<FileUploader
store={uploader!}
name={key}
{...meta}
defaultValue={banners?.map(({ uri }) => uri)}
/>
<ArrayField name="banners" defaultValue={banners} renderItem={this.renderMedia(i18n)} />
</RestForm.FieldBox>
),
},
Expand Down Expand Up @@ -148,6 +139,28 @@ export class ActivityEditor extends ObservedComponent<ActivityEditorProps, typeo
];
}

renderMedia =
({ t }: typeof i18n): ArrayFieldProps<Media>['renderItem'] =>
({ uri, name, description }) => (
<div className="d-flex align-items-center gap-2">
<FileUploader
store={fileStore}
name="uri"
accept="image/*"
multiple
defaultValue={uri ? [uri] : []}
/>
<FormField label={t('name')} name="name" defaultValue={name} />
<FormField
label={t('description')}
as="textarea"
rows={3}
name="description"
defaultValue={description}
/>
</div>
);

render() {
const i18n = this.observedContext,
{ downloading, uploading } = activityStore;
Expand Down
4 changes: 1 addition & 3 deletions components/Message/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export class AnnouncementList extends ObservedComponent<AnnouncementListProps, t
render() {
const i18n = this.observedContext;

return (
<RestTable {...this.props} translator={i18n} columns={this.columns} editable deletable />
);
return <RestTable {...this.props} translator={i18n} columns={this.columns} />;
}
}
73 changes: 21 additions & 52 deletions models/Base/File.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,34 @@
import { HTTPError, Request, request } from 'koajax';
import { DataObject, toggle } from 'mobx-restful';
import { SignedLink } from '@kaiyuanshe/openhackathon-service';
import { toggle } from 'mobx-restful';
import { FileModel } from 'mobx-restful-table';
import { blobOf, uniqueID } from 'web-utility';

import sessionStore from '../User/Session';
import { ErrorBaseData, UploadUrl } from './index';

export class AzureFileModel extends FileModel {
static async uploadBlob<T = void>(
fullPath: string,
method: Request['method'] = 'PUT',
body?: any,
headers: DataObject = {},
) {
headers['x-ms-blob-type'] = 'BlockBlob';

const { response } = request<T>({
path: fullPath,
method,
body,
headers,
});
const { headers: header, body: data } = await response;

if (!data || !('traceId' in (data as DataObject))) return data!;

const { status, title, detail } = data as unknown as ErrorBaseData;

throw new HTTPError(
detail || title,
{ method, path: fullPath, headers, body },
{ status, statusText: title, headers: header, body: data },
);
}
export class S3FileModel extends FileModel {
client = sessionStore.client;

@toggle('uploading')
async upload(file: File) {
const { type, name } = file;

const { body } = await sessionStore.client.post<UploadUrl>(
`user/generateFileUrl`,
{ filename: name },
async upload(file: string | Blob) {
if (typeof file === 'string') {
const name = file.split('/').pop()!;

file = new File([await blobOf(file)], name);
}
const { body } = await this.client.post<SignedLink>(
`file/signed-link/${file instanceof File ? file.name : uniqueID()}`,
);
const parts = body!.uploadUrl.split('/');

const path = parts.slice(0, -1).join('/'),
[fileName, data] = parts.at(-1)!.split('?');

const URI_Put = `${path}/${encodeURIComponent(fileName)}?${data}`;
await this.client.put(body!.putLink, file, { 'Content-Type': file.type });

await AzureFileModel.uploadBlob(URI_Put, 'PUT', file, {
'Content-Type': type,
});

const { origin, pathname } = new URL(body!.url);
return super.upload(body!.getLink);
}

const URI_Get = `${
origin + pathname.split('/').slice(0, -1).join('/')
}/${encodeURIComponent(fileName)}`;
@toggle('uploading')
async delete(link: string) {
await this.client.delete(`file/${link.replace(`${this.client.baseURI}/file/`, '')}`);

return super.upload(URI_Get);
await super.delete(link);
}
}

export default new AzureFileModel();
export default new S3FileModel();
14 changes: 11 additions & 3 deletions models/Base/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Base, ListChunk } from '@kaiyuanshe/openhackathon-service';
import { Filter as BaseFilter, ListModel, RESTClient } from 'mobx-restful';
import { Filter as BaseFilter, IDType, ListModel, RESTClient, toggle } from 'mobx-restful';
import { buildURLData } from 'web-utility';

import { ownClient } from '../User/Session';

export interface UploadUrl
extends Record<'filename' | 'uploadUrl' | 'url', string> {
export interface UploadUrl extends Record<'filename' | 'uploadUrl' | 'url', string> {
expiration: number;
}

Expand Down Expand Up @@ -59,6 +58,15 @@ export abstract class TableModel<
> extends ListModel<D, F> {
client = ownClient;

@toggle('uploading')
async updateOne(data: BaseFilter<D>, id?: IDType) {
const { body } = await (id
? this.client.put<D>(`${this.baseURI}/${id}`, data)
: this.client.post<D>(this.baseURI, data));

return (this.currentOne = body!);
}

async loadPage(pageIndex: number, pageSize: number, filter: F) {
const { body } = await this.client.get<ListChunk<D>>(
`${this.baseURI}?${buildURLData({ ...filter, pageIndex, pageSize })}`,
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"mobx-react": "^9.2.0",
"mobx-react-helper": "^0.4.1",
"mobx-restful": "^2.1.0",
"mobx-restful-table": "^2.5.1",
"mobx-restful-table": "^2.5.2",
"next": "^15.3.3",
"next-ssr-middleware": "^1.0.0",
"open-react-map": "^0.9.0",
Expand All @@ -49,8 +49,8 @@
"@cspell/eslint-plugin": "^9.0.2",
"@eslint/compat": "^1.2.9",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
"@kaiyuanshe/openhackathon-service": "^0.21.1",
"@eslint/js": "^9.28.0",
"@kaiyuanshe/openhackathon-service": "^0.22.0",
"@next/eslint-plugin-next": "^15.3.3",
"@octokit/openapi-types": "^25.1.0",
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
Expand All @@ -62,7 +62,7 @@
"@types/next-pwa": "^5.6.9",
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"eslint": "^9.27.0",
"eslint": "^9.28.0",
"eslint-config-next": "^15.3.3",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-react": "^7.37.5",
Expand Down
2 changes: 1 addition & 1 deletion pages/activity/[name]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const getServerSideProps = compose<{ name?: string }, ActivityPageProps>(
activityStore.organizationOf(name).getList(),
]);

return { props: { activity, organizationList } };
return { props: JSON.parse(JSON.stringify({ activity, organizationList })) };
},
);

Expand Down
Loading