Skip to content

Commit 9464c2e

Browse files
CopilotTechQuery
andcommitted
Implement complete sponsor management functionality for meeting administrators
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
1 parent ccb2dd1 commit 9464c2e

9 files changed

Lines changed: 442 additions & 0 deletions

File tree

components/Activity/menu.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ import { MenuItem } from '../User/SessionBox';
44
export const organizerMenu = ({ t }: typeof i18n, activityId: number): MenuItem[] => [
55
{ href: `/activity/${activityId}/editor`, title: t('edit_activity') },
66
{ href: `/activity/${activityId}/forum`, title: t('forum_list') },
7+
{ href: `/activity/${activityId}/sponsor`, title: t('sponsor_management') },
78
];

components/Sponsor/Editor.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { computed } from 'mobx';
2+
import { observer } from 'mobx-react';
3+
import { ObservedComponent } from 'mobx-react-helper';
4+
import { Field, RestForm } from 'mobx-restful-table';
5+
6+
import fileStore from '../../models/File';
7+
import { Sponsor, SponsorLevel, SponsorModel, SponsorStatus } from '../../models/Sponsor';
8+
import { i18n, I18nContext } from '../../models/Translation';
9+
10+
export interface SponsorEditorProps {
11+
sponsor?: Sponsor;
12+
activityId: number;
13+
}
14+
15+
@observer
16+
export class SponsorEditor extends ObservedComponent<SponsorEditorProps, typeof i18n> {
17+
static contextType = I18nContext;
18+
19+
sponsorStore = new SponsorModel(this.props.activityId);
20+
21+
componentDidMount() {
22+
const { sponsor } = this.props;
23+
24+
if (sponsor) this.sponsorStore.currentOne = sponsor;
25+
}
26+
27+
submitHandler = (data: Sponsor) => {
28+
const { activityId } = this.props;
29+
const { t } = this.observedContext;
30+
const isUpdate = !!this.props.sponsor;
31+
32+
alert(isUpdate ? t('sponsor_updated_successfully') : t('sponsor_created_successfully'));
33+
34+
window.location.href = `/activity/${activityId}/sponsor`;
35+
};
36+
37+
@computed
38+
get fields(): Field<Sponsor>[] {
39+
const { t } = this.observedContext;
40+
41+
return [
42+
{
43+
key: 'name',
44+
renderLabel: t('name'),
45+
required: true,
46+
invalidMessage: t('field_required'),
47+
},
48+
{
49+
key: 'englishName',
50+
renderLabel: t('english_name'),
51+
},
52+
{
53+
key: 'url',
54+
renderLabel: t('website'),
55+
type: 'url',
56+
},
57+
{
58+
key: 'logo',
59+
renderLabel: t('logo'),
60+
type: 'file',
61+
accept: 'image/*',
62+
uploader: fileStore,
63+
},
64+
{
65+
key: 'level',
66+
renderLabel: t('sponsor_level'),
67+
type: 'select',
68+
options: Object.values(SponsorLevel).map(level => ({
69+
title: t(`sponsor_level_${level}`),
70+
value: level,
71+
})),
72+
required: true,
73+
invalidMessage: t('field_required'),
74+
},
75+
{
76+
key: 'sponsorshipAmount',
77+
renderLabel: t('sponsorship_amount'),
78+
type: 'number',
79+
min: 0,
80+
step: 100,
81+
},
82+
{
83+
key: 'contactPerson',
84+
renderLabel: t('contact_person'),
85+
},
86+
{
87+
key: 'status',
88+
renderLabel: t('status'),
89+
type: 'select',
90+
options: Object.values(SponsorStatus).map(status => ({
91+
title: t(`sponsor_status_${status}`),
92+
value: status,
93+
})),
94+
required: true,
95+
invalidMessage: t('field_required'),
96+
},
97+
{
98+
key: 'summary',
99+
renderLabel: t('summary'),
100+
rows: 3,
101+
},
102+
{
103+
key: 'remarks',
104+
renderLabel: t('remarks'),
105+
rows: 3,
106+
},
107+
];
108+
}
109+
110+
render() {
111+
const { downloading, uploading } = this.sponsorStore;
112+
113+
const loading = downloading > 0 || uploading > 0;
114+
115+
return (
116+
<>
117+
<RestForm
118+
className="container-fluid"
119+
translator={this.observedContext}
120+
store={this.sponsorStore}
121+
fields={this.fields}
122+
onSubmit={this.submitHandler}
123+
/>
124+
{loading && <div>Loading...</div>}
125+
</>
126+
);
127+
}
128+
}

components/Sponsor/List.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { computed } from 'mobx';
2+
import { observer } from 'mobx-react';
3+
import { ObservedComponent } from 'mobx-react-helper';
4+
import { Column, RestTable } from 'mobx-restful-table';
5+
import { Container } from 'react-bootstrap';
6+
7+
import { Sponsor, SponsorLevel, SponsorModel, SponsorStatus } from '../../models/Sponsor';
8+
import { i18n, I18nContext } from '../../models/Translation';
9+
import { PageHead } from '../Navigator/PageHead';
10+
11+
export interface SponsorListProps {
12+
activityId: number;
13+
}
14+
15+
@observer
16+
export class SponsorList extends ObservedComponent<SponsorListProps, typeof i18n> {
17+
static contextType = I18nContext;
18+
19+
sponsorStore = new SponsorModel(this.props.activityId);
20+
21+
@computed
22+
get columns(): Column<Sponsor>[] {
23+
const { t } = this.observedContext,
24+
{ activityId } = this.observedProps;
25+
26+
return [
27+
{
28+
key: 'name',
29+
renderHead: t('name'),
30+
renderBody: ({ id, name }) => (
31+
<a href={`/activity/${activityId}/sponsor/${id}/editor`}>{name}</a>
32+
),
33+
required: true,
34+
minLength: 2,
35+
invalidMessage: t('field_required'),
36+
},
37+
{
38+
key: 'level',
39+
renderHead: t('sponsor_level'),
40+
renderBody: ({ level }) => level && t(`sponsor_level_${level}`),
41+
type: 'select',
42+
options: Object.values(SponsorLevel).map(level => ({
43+
title: t(`sponsor_level_${level}`),
44+
value: level,
45+
})),
46+
},
47+
{
48+
key: 'sponsorshipAmount',
49+
renderHead: t('sponsorship_amount'),
50+
type: 'number',
51+
min: 0,
52+
step: 100,
53+
},
54+
{
55+
key: 'contactPerson',
56+
renderHead: t('contact_person'),
57+
},
58+
{
59+
key: 'status',
60+
renderHead: t('status'),
61+
renderBody: ({ status }) => status && t(`sponsor_status_${status}`),
62+
type: 'select',
63+
options: Object.values(SponsorStatus).map(status => ({
64+
title: t(`sponsor_status_${status}`),
65+
value: status,
66+
})),
67+
},
68+
{
69+
key: 'url',
70+
renderHead: t('website'),
71+
type: 'url',
72+
renderBody: ({ url }) =>
73+
url && (
74+
<a href={url} target="_blank" rel="noopener noreferrer">
75+
{url}
76+
</a>
77+
),
78+
},
79+
];
80+
}
81+
82+
render() {
83+
const { t } = this.observedContext;
84+
85+
return (
86+
<Container fluid>
87+
<PageHead title={t('sponsor_management')} />
88+
89+
<RestTable
90+
className="h-100 text-center"
91+
striped
92+
hover
93+
editable
94+
deletable
95+
columns={this.columns}
96+
store={this.sponsorStore}
97+
translator={this.observedContext}
98+
/>
99+
</Container>
100+
);
101+
}
102+
}

models/Sponsor.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Organization } from '@open-source-bazaar/activityhub-service';
2+
3+
import { TableModel } from './Base';
4+
import userStore from './User';
5+
6+
// Sponsor level enumeration
7+
export enum SponsorLevel {
8+
Gold = 'gold',
9+
Silver = 'silver',
10+
Bronze = 'bronze',
11+
Platinum = 'platinum',
12+
}
13+
14+
// Sponsor status enumeration
15+
export enum SponsorStatus {
16+
Active = 'active',
17+
Pending = 'pending',
18+
Inactive = 'inactive',
19+
Rejected = 'rejected',
20+
}
21+
22+
// Extended sponsor interface based on Organization
23+
export interface Sponsor extends Organization {
24+
// Sponsor-specific fields
25+
level?: SponsorLevel;
26+
sponsorshipAmount?: number;
27+
contactPerson?: string;
28+
remarks?: string;
29+
status?: SponsorStatus;
30+
// Activity relationship
31+
activityId?: number;
32+
}
33+
34+
export class SponsorModel extends TableModel<Sponsor> {
35+
baseURI = '';
36+
client = userStore.client;
37+
38+
constructor(activityId: number) {
39+
super();
40+
// For now, using organization endpoint with activity context
41+
// This can be updated when proper sponsor endpoint is available
42+
this.baseURI = `activity/${activityId}/sponsor`;
43+
}
44+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { User } from '@open-source-bazaar/activityhub-service';
2+
import { observer } from 'mobx-react';
3+
import { useRouter } from 'next/router';
4+
import { compose, JWTProps, jwtVerifier } from 'next-ssr-middleware';
5+
import { FC, useContext } from 'react';
6+
7+
import { organizerMenu } from '../../../../../components/Activity/menu';
8+
import { SponsorEditor } from '../../../../../components/Sponsor/Editor';
9+
import { SessionBox } from '../../../../../components/User/SessionBox';
10+
import { Sponsor, SponsorModel } from '../../../../../models/Sponsor';
11+
import { I18nContext } from '../../../../../models/Translation';
12+
13+
interface SponsorEditorPageProps extends JWTProps<User> {
14+
sponsor?: Sponsor;
15+
}
16+
17+
export const getServerSideProps = compose<{ id: string; sid: string }, SponsorEditorPageProps>(
18+
jwtVerifier(),
19+
async ({ params }) => {
20+
if (!+params!.id) return { props: {} };
21+
22+
try {
23+
const sponsorStore = new SponsorModel(+params!.id);
24+
const sponsor = await sponsorStore.getOne(+params!.sid);
25+
26+
return { props: { sponsor } };
27+
} catch {
28+
return { props: {} };
29+
}
30+
},
31+
);
32+
33+
const SponsorEditorPage: FC<SponsorEditorPageProps> = observer(({ jwtPayload, sponsor }) => {
34+
const { asPath, query } = useRouter(),
35+
i18n = useContext(I18nContext);
36+
37+
const activityId = +(query.id as string);
38+
const title = sponsor ? i18n.t('edit_sponsor') : i18n.t('create_sponsor');
39+
40+
return (
41+
<SessionBox {...{ title, jwtPayload }} path={asPath} menu={organizerMenu(i18n, activityId)}>
42+
<SponsorEditor sponsor={sponsor} activityId={activityId} />
43+
</SessionBox>
44+
);
45+
});
46+
47+
export default SponsorEditorPage;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { User } from '@open-source-bazaar/activityhub-service';
2+
import { observer } from 'mobx-react';
3+
import { useRouter } from 'next/router';
4+
import { compose, JWTProps, jwtVerifier } from 'next-ssr-middleware';
5+
import { FC, useContext } from 'react';
6+
7+
import { organizerMenu } from '../../../../components/Activity/menu';
8+
import { SponsorList } from '../../../../components/Sponsor/List';
9+
import { SessionBox } from '../../../../components/User/SessionBox';
10+
import { I18nContext } from '../../../../models/Translation';
11+
12+
interface SponsorListPageProps extends JWTProps<User> {}
13+
14+
export const getServerSideProps = compose<{ id: string }, SponsorListPageProps>(jwtVerifier());
15+
16+
const SponsorListPage: FC<SponsorListPageProps> = observer(({ jwtPayload }) => {
17+
const { asPath, query } = useRouter(),
18+
i18n = useContext(I18nContext);
19+
20+
const activityId = +(query.id as string);
21+
const title = i18n.t('sponsor_management');
22+
23+
return (
24+
<SessionBox {...{ title, jwtPayload }} path={asPath} menu={organizerMenu(i18n, activityId)}>
25+
<SponsorList activityId={activityId} />
26+
</SessionBox>
27+
);
28+
});
29+
30+
export default SponsorListPage;

0 commit comments

Comments
 (0)