diff --git a/admin/src/App.tsx b/admin/src/App.tsx
index c155c354..999271d7 100644
--- a/admin/src/App.tsx
+++ b/admin/src/App.tsx
@@ -5,6 +5,8 @@ import { Organizations } from './components/Organizations';
import { ErrorAlert } from './components/ErrorAlert';
import { Users } from './components/Users';
import { Notifications } from './components/Notifications';
+import { Feedback } from './components/Feedback';
+import { BearItems } from './components/BearItems';
import {
AppBar,
@@ -30,6 +32,8 @@ import {
faBuilding,
faPerson,
faBell,
+ faComment,
+ faPaw,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -91,6 +95,18 @@ const routes = [
icon: faBell,
name: 'Notifications',
},
+ {
+ path: '/feedback',
+ element: ,
+ icon: faComment,
+ name: 'Feedback',
+ },
+ {
+ path: '/bear-items',
+ element: ,
+ icon: faPaw,
+ name: 'Bear Items',
+ },
];
const SidebarIcon = styled.span`
diff --git a/admin/src/all.dto.ts b/admin/src/all.dto.ts
index 8544a099..e1c54b7a 100644
--- a/admin/src/all.dto.ts
+++ b/admin/src/all.dto.ts
@@ -29,6 +29,90 @@ export enum BearSlotDto {
ACCESSORY = 'ACCESSORY',
}
+export enum CampusEventCategoryDto {
+ SOCIAL = 'SOCIAL',
+ CULTURAL = 'CULTURAL',
+ ATHLETIC = 'ATHLETIC',
+ WELLNESS = 'WELLNESS',
+ ACADEMIC = 'ACADEMIC',
+ ARTS = 'ARTS',
+ CAREER = 'CAREER',
+ COMMUNITY = 'COMMUNITY',
+ OTHER = 'OTHER',
+}
+
+export enum EventSourceDto {
+ API_EVENTS = 'API_EVENTS',
+ ADMIN_CREATED = 'ADMIN_CREATED',
+ COMMUNITY_SUBMITTED = 'COMMUNITY_SUBMITTED',
+}
+
+export enum CheckInMethodDto {
+ LOCATION = 'LOCATION',
+ QR_CODE = 'QR_CODE',
+ EITHER = 'EITHER',
+}
+
+export enum CampusEventCategoriesDto {
+ SOCIAL = 'SOCIAL',
+ CULTURAL = 'CULTURAL',
+ ATHLETIC = 'ATHLETIC',
+ WELLNESS = 'WELLNESS',
+ ACADEMIC = 'ACADEMIC',
+ ARTS = 'ARTS',
+ CAREER = 'CAREER',
+ COMMUNITY = 'COMMUNITY',
+ OTHER = 'OTHER',
+}
+
+export enum CampusEventSourceDto {
+ API_EVENTS = 'API_EVENTS',
+ ADMIN_CREATED = 'ADMIN_CREATED',
+ COMMUNITY_SUBMITTED = 'COMMUNITY_SUBMITTED',
+}
+
+export enum CampusEventCheckInMethodDto {
+ LOCATION = 'LOCATION',
+ QR_CODE = 'QR_CODE',
+ EITHER = 'EITHER',
+}
+
+export enum RequestCampusEventsCategoriesDto {
+ SOCIAL = 'SOCIAL',
+ CULTURAL = 'CULTURAL',
+ ATHLETIC = 'ATHLETIC',
+ WELLNESS = 'WELLNESS',
+ ACADEMIC = 'ACADEMIC',
+ ARTS = 'ARTS',
+ CAREER = 'CAREER',
+ COMMUNITY = 'COMMUNITY',
+ OTHER = 'OTHER',
+}
+
+export enum UpsertCampusEventCategoriesDto {
+ SOCIAL = 'SOCIAL',
+ CULTURAL = 'CULTURAL',
+ ATHLETIC = 'ATHLETIC',
+ WELLNESS = 'WELLNESS',
+ ACADEMIC = 'ACADEMIC',
+ ARTS = 'ARTS',
+ CAREER = 'CAREER',
+ COMMUNITY = 'COMMUNITY',
+ OTHER = 'OTHER',
+}
+
+export enum UpsertCampusEventSourceDto {
+ API_EVENTS = 'API_EVENTS',
+ ADMIN_CREATED = 'ADMIN_CREATED',
+ COMMUNITY_SUBMITTED = 'COMMUNITY_SUBMITTED',
+}
+
+export enum UpsertCampusEventCheckInMethodDto {
+ LOCATION = 'LOCATION',
+ QR_CODE = 'QR_CODE',
+ EITHER = 'EITHER',
+}
+
export enum ChallengeLocationDto {
ENG_QUAD = 'ENG_QUAD',
ARTS_QUAD = 'ARTS_QUAD',
@@ -43,13 +127,45 @@ export enum ChallengeLocationDto {
ANY = 'ANY',
}
+export enum CheckInResultCheckInMethodDto {
+ LOCATION = 'LOCATION',
+ QR_CODE = 'QR_CODE',
+}
+
+export enum CheckInErrorCodeDto {
+ EVENT_NOT_FOUND = 'EVENT_NOT_FOUND',
+ EVENT_NOT_ACTIVE = 'EVENT_NOT_ACTIVE',
+ EVENT_NOT_APPROVED = 'EVENT_NOT_APPROVED',
+ ALREADY_CHECKED_IN = 'ALREADY_CHECKED_IN',
+ OUT_OF_RADIUS = 'OUT_OF_RADIUS',
+ METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
+ INVALID_QR_CODE = 'INVALID_QR_CODE',
+ UNKNOWN_ERROR = 'UNKNOWN_ERROR',
+}
+
+export enum ClubSubmissionCategoryDto {
+ SOCIAL = 'SOCIAL',
+ CULTURAL = 'CULTURAL',
+ ATHLETIC = 'ATHLETIC',
+ WELLNESS = 'WELLNESS',
+ ACADEMIC = 'ACADEMIC',
+ ARTS = 'ARTS',
+ CAREER = 'CAREER',
+ COMMUNITY = 'COMMUNITY',
+ OTHER = 'OTHER',
+}
+
export enum EventCategoryDto {
FOOD = 'FOOD',
NATURE = 'NATURE',
HISTORICAL = 'HISTORICAL',
- CAFE = 'CAFE',
- DININGHALL = 'DININGHALL',
- DORM = 'DORM',
+ RESIDENTIAL = 'RESIDENTIAL',
+ LANDMARK = 'LANDMARK',
+ ARTS = 'ARTS',
+ ATHLETICS = 'ATHLETICS',
+ LIBRARY = 'LIBRARY',
+ ACADEMIC = 'ACADEMIC',
+ RECREATION = 'RECREATION',
}
export enum EventTimeLimitationDto {
@@ -63,6 +179,12 @@ export enum EventDifficultyDto {
Hard = 'Hard',
}
+export enum FeedbackCategoryDto {
+ BUG_REPORT = 'BUG_REPORT',
+ SUGGESTION = 'SUGGESTION',
+ GENERAL = 'GENERAL',
+}
+
export enum QuizErrorCodeDto {
NO_QUESTIONS = 'NO_QUESTIONS',
ALREADY_ANSWERED = 'ALREADY_ANSWERED',
@@ -210,6 +332,119 @@ export interface UpdatePurchaseResultDto {
itemId: string;
}
+export interface AdminBearItemDto {
+ id: string;
+ name?: string;
+ slot?: BearSlotDto;
+ cost?: number;
+ assetKey?: string;
+ mimeType?: string;
+ zIndex?: number;
+ isDefault?: boolean;
+}
+
+export interface UpdateBearItemDataDto {
+ bearItem: AdminBearItemDto;
+ deleted: boolean;
+}
+
+export interface RequestAllBearItemsDto {}
+
+export interface CampusEventDto {
+ id: string;
+ title: string;
+ description: string;
+ imageUrl?: string;
+ startTime: string;
+ endTime: string;
+ allDay: boolean;
+ locationName: string;
+ address?: string;
+ latitude: number;
+ longitude: number;
+ categories: CampusEventCategoriesDto[];
+ tags: string[];
+ source: CampusEventSourceDto;
+ externalUrl?: string;
+ organizerName?: string;
+ registrationUrl?: string;
+ checkInMethod: CampusEventCheckInMethodDto;
+ pointsForAttendance: number;
+ featured: boolean;
+ attendanceCount: number;
+ rsvpCount: number;
+}
+
+export interface RequestCampusEventsDto {
+ page: number;
+ limit: number;
+ dateFrom?: string;
+ dateTo?: string;
+ categories?: RequestCampusEventsCategoriesDto[];
+ search?: string;
+ featured?: boolean;
+}
+
+export interface CampusEventListDto {
+ events: CampusEventDto[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export interface RequestCampusEventDetailsDto {
+ eventId: string;
+}
+
+export interface UpsertCampusEventDto {
+ id?: string;
+ title: string;
+ description: string;
+ imageUrl?: string;
+ startTime: string;
+ endTime: string;
+ allDay?: boolean;
+ locationName: string;
+ address?: string;
+ latitude: number;
+ longitude: number;
+ checkInRadius?: number;
+ categories: UpsertCampusEventCategoriesDto[];
+ tags: string[];
+ source: UpsertCampusEventSourceDto;
+ externalId?: string;
+ externalUrl?: string;
+ organizerName?: string;
+ organizerEmail?: string;
+ organizerId?: string;
+ checkInMethod?: UpsertCampusEventCheckInMethodDto;
+ pointsForAttendance?: number;
+ featured?: boolean;
+ registrationUrl?: string;
+}
+
+export interface DeleteCampusEventDto {
+ eventId: string;
+}
+
+export interface RsvpCampusEventDto {
+ eventId: string;
+}
+
+export interface UnRsvpCampusEventDto {
+ eventId: string;
+}
+
+export interface UpdateCampusEventDataDto {
+ event: CampusEventDto;
+ deleted: boolean;
+}
+
+export interface CampusEventListResponseDto {
+ list: CampusEventListDto;
+}
+
export interface CompletedChallengeDto {}
export interface ChallengeDto {
@@ -225,6 +460,8 @@ export interface ChallengeDto {
closeRadiusF?: number;
linkedEventId?: string;
timerLength?: number;
+ scheduledStartTime?: string;
+ scheduledEndTime?: string;
}
export interface RequestChallengeDataDto {
@@ -250,11 +487,54 @@ export interface AvailableChallengesResponseDto {
challenges: ChallengeDto[];
}
+export interface LocationCheckInDto {
+ campusEventId: string;
+ latitude: number;
+ longitude: number;
+}
+
+export interface QrCodeCheckInDto {
+ qrCode: string;
+}
+
+export interface GenerateQrCodeDto {
+ campusEventId: string;
+}
+
+export interface CheckInResultDto {
+ attendanceId: string;
+ campusEventId: string;
+ checkInMethod: CheckInResultCheckInMethodDto;
+ pointsAwarded: number;
+ newTotalScore: number;
+}
+
+export interface CheckInErrorDto {
+ message: string;
+ code: CheckInErrorCodeDto;
+}
+
export interface UpdateErrorDto {
id: string;
message: string;
}
+export interface ClubSubmissionDto {
+ clubName: string;
+ contactEmail: string;
+ eventTitle: string;
+ description: string;
+ startTime: string;
+ endTime: string;
+ location: string;
+ latitude?: number;
+ longitude?: number;
+ category: ClubSubmissionCategoryDto;
+ address?: string;
+ imageUrl?: string;
+ registrationLink?: string;
+}
+
export interface RequestFilteredEventsDto {
difficulty: string[];
location: string[];
@@ -323,6 +603,7 @@ export interface PrevChallengeDto {
extensionsUsed?: number;
dateCompleted: string;
failed?: boolean;
+ dateExpired?: boolean;
}
export interface EventTrackerDto {
@@ -344,6 +625,48 @@ export interface UpdateEventDataDto {
export interface UseEventTrackerHintDto {}
+export interface TriggerEventSyncDto {
+ days?: number;
+}
+
+export interface EventSyncResultDto {
+ created: number;
+ updated: number;
+ archived: number;
+ totalFetched: number;
+ syncedAt: string;
+}
+
+export interface RequestEventSyncStatusDto {}
+
+export interface UpdateEventSyncStatusDto {
+ running: boolean;
+ lastResult: EventSyncResultDto;
+}
+
+export interface SubmitFeedbackDto {
+ category: FeedbackCategoryDto;
+ text: string;
+ rating?: boolean;
+ challengeId?: string;
+}
+
+export interface FeedbackDto {
+ id: string;
+ createdAt: string;
+ category: FeedbackCategoryDto;
+ text: string;
+ rating?: boolean;
+ challengeId?: string;
+ userId: string;
+ username?: string;
+ challengeName?: string;
+}
+
+export interface UpdateFeedbackDataDto {
+ feedbacks: FeedbackDto[];
+}
+
export interface JoinGroupDto {
groupId: string;
}
diff --git a/admin/src/components/BearItems.tsx b/admin/src/components/BearItems.tsx
new file mode 100644
index 00000000..29c6c9ee
--- /dev/null
+++ b/admin/src/components/BearItems.tsx
@@ -0,0 +1,207 @@
+import { useContext, useState } from 'react';
+import { DeleteModal } from './DeleteModal';
+import {
+ EntryModal,
+ EntryForm,
+ NumberEntryForm,
+ OptionEntryForm,
+ FreeEntryForm,
+} from './EntryModal';
+import { HButton } from './HButton';
+import {
+ CenterText,
+ ListCardBody,
+ ListCardBox,
+ ListCardButtons,
+ ListCardTitle,
+} from './ListCard';
+import { SearchBar } from './SearchBar';
+import { ServerDataContext } from './ServerData';
+
+import { compareTwoStrings } from 'string-similarity';
+import { AdminBearItemDto, BearSlotDto } from '../all.dto';
+
+const slotOptions = [
+ BearSlotDto.EYES,
+ BearSlotDto.MOUTH,
+ BearSlotDto.COLOR,
+ BearSlotDto.ACCESSORY,
+];
+
+const defaultOptions = ['No', 'Yes'];
+
+function BearItemCard(props: {
+ item: AdminBearItemDto;
+ onEdit: () => void;
+ onDelete: () => void;
+}) {
+ return (
+
+ {props.item.name}
+
+ Id: {props.item.id}
+ Slot: {props.item.slot}
+ Cost: {props.item.cost}
+ Asset Key: {props.item.assetKey}
+ MIME Type: {props.item.mimeType || 'N/A'}
+ Z-Index: {props.item.zIndex ?? 'N/A'}
+ Default: {props.item.isDefault ? 'Yes' : 'No'}
+
+
+ DELETE
+
+ EDIT
+
+
+
+ );
+}
+
+function makeForm(): EntryForm[] {
+ return [
+ { name: 'Name', characterLimit: 256, value: '' },
+ {
+ name: 'Slot',
+ options: slotOptions as string[],
+ value: 0,
+ },
+ { name: 'Cost', value: 0, min: 0, max: 99999 },
+ { name: 'Asset Key', characterLimit: 512, value: '' },
+ { name: 'MIME Type', characterLimit: 128, value: '' },
+ { name: 'Z-Index', value: 0, min: -100, max: 100 },
+ {
+ name: 'Is Default',
+ options: defaultOptions,
+ value: 0,
+ },
+ ];
+}
+
+function fromForm(form: EntryForm[], id: string): AdminBearItemDto {
+ return {
+ id,
+ name: (form[0] as FreeEntryForm).value,
+ slot: slotOptions[(form[1] as OptionEntryForm).value],
+ cost: (form[2] as NumberEntryForm).value,
+ assetKey: (form[3] as FreeEntryForm).value,
+ mimeType: (form[4] as FreeEntryForm).value,
+ zIndex: (form[5] as NumberEntryForm).value,
+ isDefault: (form[6] as OptionEntryForm).value === 1,
+ };
+}
+
+function toForm(item: AdminBearItemDto): EntryForm[] {
+ return [
+ { name: 'Name', characterLimit: 256, value: item.name ?? '' },
+ {
+ name: 'Slot',
+ options: slotOptions as string[],
+ value: slotOptions.indexOf(item.slot ?? BearSlotDto.ACCESSORY),
+ },
+ { name: 'Cost', value: item.cost ?? 0, min: 0, max: 99999 },
+ { name: 'Asset Key', characterLimit: 512, value: item.assetKey ?? '' },
+ { name: 'MIME Type', characterLimit: 128, value: item.mimeType ?? '' },
+ { name: 'Z-Index', value: item.zIndex ?? 0, min: -100, max: 100 },
+ {
+ name: 'Is Default',
+ options: defaultOptions,
+ value: item.isDefault ? 1 : 0,
+ },
+ ];
+}
+
+export function BearItems() {
+ const serverData = useContext(ServerDataContext);
+ const [isCreateModalOpen, setCreateModalOpen] = useState(false);
+ const [isEditModalOpen, setEditModalOpen] = useState(false);
+ const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
+
+ const [form, setForm] = useState(() => makeForm());
+ const [currentId, setCurrentId] = useState('');
+ const [query, setQuery] = useState('');
+
+ const allItems = Array.from(serverData.bearItems.values());
+
+ return (
+ <>
+ {
+ serverData.updateBearItem(fromForm(form, ''));
+ setCreateModalOpen(false);
+ }}
+ onCancel={() => setCreateModalOpen(false)}
+ form={form}
+ />
+ {
+ serverData.updateBearItem(fromForm(form, currentId));
+ setEditModalOpen(false);
+ }}
+ onCancel={() => setEditModalOpen(false)}
+ form={form}
+ />
+ setDeleteModalOpen(false)}
+ onDelete={() => {
+ serverData.deleteBearItem(currentId);
+ setDeleteModalOpen(false);
+ }}
+ />
+ {
+ setForm(makeForm());
+ setCreateModalOpen(true);
+ }}
+ onSearch={q => setQuery(q)}
+ />
+ {allItems.length === 0 && No bear items found}
+ {allItems
+ .filter(item => {
+ if (query === '') return true;
+ const q = query.toLowerCase();
+ return (
+ (item.name ?? '').toLowerCase().includes(q) ||
+ (item.slot ?? '').toLowerCase().includes(q) ||
+ (item.assetKey ?? '').toLowerCase().includes(q)
+ );
+ })
+ .sort((a, b) => {
+ if (query === '') {
+ const slotCmp = (a.slot ?? '').localeCompare(b.slot ?? '');
+ if (slotCmp !== 0) return slotCmp;
+ return (a.name ?? '').localeCompare(b.name ?? '');
+ }
+ return (
+ compareTwoStrings(b.name ?? '', query) -
+ compareTwoStrings(a.name ?? '', query)
+ );
+ })
+ .map(item => (
+ {
+ setCurrentId(item.id);
+ setDeleteModalOpen(true);
+ }}
+ onEdit={() => {
+ const fresh = serverData.bearItems.get(item.id);
+ if (fresh) {
+ setCurrentId(item.id);
+ setForm(toForm(fresh));
+ setEditModalOpen(true);
+ }
+ }}
+ />
+ ))}
+ >
+ );
+}
diff --git a/admin/src/components/ChallengeCardComponents.tsx b/admin/src/components/ChallengeCardComponents.tsx
index 3c87cef6..2359ac68 100644
--- a/admin/src/components/ChallengeCardComponents.tsx
+++ b/admin/src/components/ChallengeCardComponents.tsx
@@ -12,6 +12,7 @@ import {
OptionEntryForm,
MapEntryForm,
CheckboxNumberEntryForm,
+ CheckboxDateEntryForm,
AnswersEntryForm,
OptionWithCustomEntryForm,
} from './EntryModal';
@@ -223,6 +224,16 @@ export function makeChallengeForm(): EntryForm[] {
max: 3600,
numberLabel: 'Timer Length (seconds)',
},
+ {
+ name: 'Scheduled Start Time',
+ checked: false,
+ date: new Date(),
+ },
+ {
+ name: 'Scheduled End Time',
+ checked: false,
+ date: new Date(),
+ },
];
}
@@ -285,6 +296,20 @@ export function challengeToForm(challenge: ChallengeDto) {
max: 3600,
numberLabel: 'Timer Length (seconds)',
},
+ {
+ name: 'Scheduled Start Time',
+ checked: !!challenge.scheduledStartTime,
+ date: challenge.scheduledStartTime
+ ? new Date(challenge.scheduledStartTime)
+ : new Date(),
+ },
+ {
+ name: 'Scheduled End Time',
+ checked: !!challenge.scheduledEndTime,
+ date: challenge.scheduledEndTime
+ ? new Date(challenge.scheduledEndTime)
+ : new Date(),
+ },
];
}
@@ -294,6 +319,8 @@ export function challengeFromForm(
id: string,
): ChallengeDto {
const timerForm = form[8] as CheckboxNumberEntryForm;
+ const startForm = form[9] as CheckboxDateEntryForm;
+ const endForm = form[10] as CheckboxDateEntryForm;
return {
id,
name: (form[2] as FreeEntryForm).value,
@@ -307,6 +334,10 @@ export function challengeFromForm(
closeRadiusF: (form[7] as NumberEntryForm).value,
linkedEventId: eventId,
timerLength: timerForm.checked ? timerForm.value : undefined,
+ scheduledStartTime: startForm.checked
+ ? startForm.date.toISOString()
+ : undefined,
+ scheduledEndTime: endForm.checked ? endForm.date.toISOString() : undefined,
};
}
@@ -412,6 +443,14 @@ export function ChallengeCard(props: {
? `${Math.floor(props.challenge.timerLength / 60)}m ${props.challenge.timerLength % 60}s`
: 'None'}
+
+ Scheduled:{' '}
+
+ {props.challenge.scheduledStartTime ||
+ props.challenge.scheduledEndTime
+ ? `${props.challenge.scheduledStartTime ? new Date(props.challenge.scheduledStartTime).toLocaleString() : '—'} to ${props.challenge.scheduledEndTime ? new Date(props.challenge.scheduledEndTime).toLocaleString() : '—'}`
+ : 'None'}
+
UP
diff --git a/admin/src/components/Challenges.tsx b/admin/src/components/Challenges.tsx
index 93166685..a97340ac 100644
--- a/admin/src/components/Challenges.tsx
+++ b/admin/src/components/Challenges.tsx
@@ -45,9 +45,13 @@ const eventCategoryOptions = [
'FOOD',
'NATURE',
'HISTORICAL',
- 'CAFE',
- 'DININGHALL',
- 'DORM',
+ 'RESIDENTIAL',
+ 'LANDMARK',
+ 'ARTS',
+ 'ATHLETICS',
+ 'LIBRARY',
+ 'ACADEMIC',
+ 'RECREATION',
];
// Combined form indices:
diff --git a/admin/src/components/EntryModal.tsx b/admin/src/components/EntryModal.tsx
index 0c63d374..98ecf35e 100644
--- a/admin/src/components/EntryModal.tsx
+++ b/admin/src/components/EntryModal.tsx
@@ -60,6 +60,12 @@ export type AnswersEntryForm = {
maxAnswers: number;
};
+export type CheckboxDateEntryForm = {
+ name: string;
+ checked: boolean;
+ date: Date;
+};
+
export type OptionWithCustomEntryForm = {
name: string;
value: number;
@@ -76,7 +82,8 @@ export type EntryForm =
| DateEntryForm
| CheckboxNumberEntryForm
| AnswersEntryForm
- | OptionWithCustomEntryForm;
+ | OptionWithCustomEntryForm
+ | CheckboxDateEntryForm;
const EntryBox = styled.div`
margin-bottom: 12px;
@@ -256,6 +263,46 @@ function CheckboxNumberEntryFormBox(props: { form: CheckboxNumberEntryForm }) {
);
}
+function CheckboxDateEntryFormBox(props: { form: CheckboxDateEntryForm }) {
+ const [checked, setChecked] = useState(props.form.checked);
+ const [val, setVal] = useState('');
+
+ useEffect(() => {
+ setChecked(props.form.checked);
+ setVal(props.form.date.toISOString().slice(0, 16));
+ }, [props.form]);
+
+ return (
+ <>
+
+
+
+ {checked && (
+
+ {
+ setVal(e.target.value);
+ props.form.date = new Date(e.target.value);
+ }}
+ />
+
+ )}
+ >
+ );
+}
+
const MapBox = styled.div`
width: 100%;
height: 300px;
@@ -270,9 +317,81 @@ const mapContainerStyle = {
height: '300px',
};
+type FrontendConfigResponse = {
+ googleMapsApiKey?: string;
+};
+
function MapEntryFormBox(props: {
form: MapEntryForm;
allForms?: EntryForm[];
+}) {
+ const [googleMapsApiKey, setGoogleMapsApiKey] = useState(
+ process.env.REACT_APP_GOOGLE_MAPS_API_KEY || '',
+ );
+ const [hasLoadedConfig, setHasLoadedConfig] = useState(
+ Boolean(process.env.REACT_APP_GOOGLE_MAPS_API_KEY),
+ );
+
+ useEffect(() => {
+ if (googleMapsApiKey) {
+ setHasLoadedConfig(true);
+ return;
+ }
+
+ let isCancelled = false;
+
+ fetch('/frontend-config')
+ .then(async response => {
+ if (!response.ok) {
+ throw new Error(`Failed to load config: ${response.status}`);
+ }
+
+ const config = (await response.json()) as FrontendConfigResponse;
+
+ if (isCancelled) {
+ return;
+ }
+
+ setGoogleMapsApiKey(config.googleMapsApiKey || '');
+ setHasLoadedConfig(true);
+ })
+ .catch(() => {
+ if (!isCancelled) {
+ setHasLoadedConfig(true);
+ }
+ });
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [googleMapsApiKey]);
+
+ if (!hasLoadedConfig) {
+ return Loading map configuration...;
+ }
+
+ if (!googleMapsApiKey) {
+ return (
+
+ Google Maps is unavailable. Set `REACT_APP_GOOGLE_MAPS_API_KEY` in the
+ running server environment.
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+function LoadedMapEntryFormBox(props: {
+ form: MapEntryForm;
+ allForms?: EntryForm[];
+ googleMapsApiKey: string;
}) {
const [lat, setLat] = useState(props.form.latitude);
const [lng, setLng] = useState(props.form.longitude);
@@ -282,8 +401,8 @@ function MapEntryFormBox(props: {
lng: props.form.longitude,
});
- const { isLoaded } = useJsApiLoader({
- googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY || '',
+ const { isLoaded, loadError } = useJsApiLoader({
+ googleMapsApiKey: props.googleMapsApiKey,
});
// Derive radii from form so map updates when Awarding/Close Distance fields change
@@ -343,6 +462,15 @@ function MapEntryFormBox(props: {
}
};
+ if (loadError) {
+ return (
+
+ Unable to load Google Maps. Verify the API key, referrer restrictions,
+ and that the Maps JavaScript API is enabled.
+
+ );
+ }
+
if (!isLoaded) return Loading map...;
return (
@@ -607,6 +735,8 @@ export function EntryModal(props: {
return ;
} else if ('characterLimit' in form) {
return ;
+ } else if ('date' in form && 'checked' in form) {
+ return ;
} else if ('checked' in form) {
return ;
} else if ('min' in form) {
diff --git a/admin/src/components/Feedback.tsx b/admin/src/components/Feedback.tsx
new file mode 100644
index 00000000..eba84459
--- /dev/null
+++ b/admin/src/components/Feedback.tsx
@@ -0,0 +1,157 @@
+import { useContext, useEffect, useMemo, useState } from 'react';
+import styled from 'styled-components';
+import { ServerConnectionContext } from './ServerConnection';
+import { ServerApi } from './ServerApi';
+import { FeedbackDto } from '../all.dto';
+import {
+ ListCardBox,
+ ListCardTitle,
+ ListCardBody,
+ CenterText,
+} from './ListCard';
+
+const SearchBarBox = styled.div`
+ display: flex;
+ flex-direction: row;
+ position: sticky;
+ justify-content: space-between;
+ top: 0;
+ border-radius: 6px;
+ width: 100%;
+ height: 48px;
+ box-shadow: 0 0 2px black;
+ padding: 6px;
+ margin-bottom: 12px;
+ line-height: 30px;
+ font-size: 18px;
+ background-color: white;
+ opacity: 0.9;
+ z-index: 10;
+`;
+
+const SearchTextBox = styled.input`
+ flex-shrink: 1;
+ margin-left: 12px;
+ width: calc(100% - 12px);
+ font-size: 18px;
+ justify-self: flex-end;
+`;
+
+const CategoryBadge = styled.span<{ category: string }>`
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: bold;
+ color: white;
+ background-color: ${props => {
+ switch (props.category) {
+ case 'BUG_REPORT':
+ return '#e74c3c';
+ case 'SUGGESTION':
+ return '#3498db';
+ case 'LIKE':
+ return '#27ae60';
+ default:
+ return '#95a5a6';
+ }
+ }};
+`;
+
+const FeedbackMeta = styled.div`
+ color: gray;
+ font-size: 14px;
+ margin-bottom: 8px;
+`;
+
+const RatingIcon = styled.span`
+ margin-left: 8px;
+`;
+
+export function Feedback() {
+ const connection = useContext(ServerConnectionContext);
+ const [feedbacks, setFeedbacks] = useState([]);
+ const [query, setQuery] = useState('');
+
+ const sock = useMemo(
+ () => (connection.connection ? new ServerApi(connection.connection) : null),
+ [connection],
+ );
+
+ useEffect(() => {
+ if (!sock) return;
+
+ sock.onUpdateFeedbackData(data => {
+ setFeedbacks(data.feedbacks);
+ });
+
+ sock.requestFeedbackData();
+ }, [sock]);
+
+ const filtered = feedbacks.filter(
+ f =>
+ f.text.toLowerCase().includes(query.toLowerCase()) ||
+ (f.username ?? '').toLowerCase().includes(query.toLowerCase()) ||
+ f.category.toLowerCase().includes(query.toLowerCase()),
+ );
+
+ const categoryLabel = (f: FeedbackDto) => {
+ if (f.rating === true && f.text === 'Liked this challenge') return 'Like';
+ switch (f.category) {
+ case 'BUG_REPORT':
+ return 'Bug Report';
+ case 'SUGGESTION':
+ return 'Suggestion';
+ default:
+ return 'General';
+ }
+ };
+
+ const categoryForBadge = (f: FeedbackDto) => {
+ if (f.rating === true && f.text === 'Liked this challenge') return 'LIKE';
+ return f.category;
+ };
+
+ return (
+ <>
+
+ setQuery(e.target.value)}
+ />
+
+ {filtered.length === 0 ? (
+ No feedback found
+ ) : (
+ filtered.map(f => (
+
+
+
+ {categoryLabel(f)}
+
+ {f.rating != null && (
+ {f.rating ? '👍' : '👎'}
+ )}
+
+
+ {f.rating === true && f.text === 'Liked this challenge'
+ ? `${f.username ?? 'User'} liked this challenge`
+ : f.text}
+
+
+ {f.username ?? f.userId} ·{' '}
+ {new Date(f.createdAt).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ })}
+ {f.challengeName && <> · {f.challengeName}>}
+
+
+ ))
+ )}
+ >
+ );
+}
diff --git a/admin/src/components/Journeys.tsx b/admin/src/components/Journeys.tsx
index 63abbcba..09154e30 100644
--- a/admin/src/components/Journeys.tsx
+++ b/admin/src/components/Journeys.tsx
@@ -48,9 +48,13 @@ const categoryOptions = [
'FOOD',
'NATURE',
'HISTORICAL',
- 'CAFE',
- 'DININGHALL',
- 'DORM',
+ 'RESIDENTIAL',
+ 'LANDMARK',
+ 'ARTS',
+ 'ATHLETICS',
+ 'LIBRARY',
+ 'ACADEMIC',
+ 'RECREATION',
];
// Event form helpers
diff --git a/admin/src/components/ServerApi.tsx b/admin/src/components/ServerApi.tsx
index 41a66f25..cb57926f 100644
--- a/admin/src/components/ServerApi.tsx
+++ b/admin/src/components/ServerApi.tsx
@@ -55,6 +55,64 @@ export class ServerApi {
return this.send('equipBearItem', data) as Promise;
}
+ requestAllBearItems(data: dto.RequestAllBearItemsDto) {
+ return this.send('requestAllBearItems', data) as Promise<
+ number | undefined
+ >;
+ }
+
+ updateBearItemData(data: dto.UpdateBearItemDataDto) {
+ return this.send('updateBearItemData', data) as Promise;
+ }
+
+ requestCampusEvents(data: dto.RequestCampusEventsDto) {
+ return this.send('requestCampusEvents', data) as Promise<
+ number | undefined
+ >;
+ }
+
+ requestCampusEventDetails(data: dto.RequestCampusEventDetailsDto) {
+ return this.send('requestCampusEventDetails', data) as Promise<
+ string | undefined
+ >;
+ }
+
+ requestAllCampusEvents(data: dto.RequestCampusEventsDto) {
+ return this.send('requestAllCampusEvents', data) as Promise<
+ number | undefined
+ >;
+ }
+
+ createCampusEvent(data: dto.UpsertCampusEventDto) {
+ return this.send('createCampusEvent', data) as Promise;
+ }
+
+ updateCampusEvent(data: dto.UpsertCampusEventDto) {
+ return this.send('updateCampusEvent', data) as Promise;
+ }
+
+ deleteCampusEvent(data: dto.DeleteCampusEventDto) {
+ return this.send('deleteCampusEvent', data) as Promise;
+ }
+
+ rsvpCampusEvent(data: dto.RsvpCampusEventDto) {
+ return this.send('rsvpCampusEvent', data) as Promise;
+ }
+
+ unRsvpCampusEvent(data: dto.UnRsvpCampusEventDto) {
+ return this.send('unRsvpCampusEvent', data) as Promise;
+ }
+
+ requestAvailableChallenges(data: dto.RequestAvailableChallengesDto) {
+ return this.send('requestAvailableChallenges', data) as Promise<
+ any | undefined
+ >;
+ }
+
+ setCurrentChallenge(data: dto.SetCurrentChallengeDto) {
+ return this.send('setCurrentChallenge', data) as Promise;
+ }
+
requestChallengeData(data: dto.RequestChallengeDataDto) {
return this.send('requestChallengeData', data) as Promise<
number | undefined
@@ -71,6 +129,16 @@ export class ServerApi {
>;
}
+ checkInWithLocation(data: dto.LocationCheckInDto) {
+ return this.send('checkInWithLocation', data) as Promise<
+ boolean | undefined
+ >;
+ }
+
+ checkInWithQrCode(data: dto.QrCodeCheckInDto) {
+ return this.send('checkInWithQrCode', data) as Promise;
+ }
+
requestEventData(data: dto.RequestEventDataDto) {
return this.send('requestEventData', data) as Promise;
}
@@ -109,6 +177,22 @@ export class ServerApi {
return this.send('updateEventData', data) as Promise;
}
+ triggerEventSync(data: dto.TriggerEventSyncDto) {
+ return this.send('triggerEventSync', data) as Promise;
+ }
+
+ requestEventSyncStatus() {
+ return this.send('requestEventSyncStatus', {}) as Promise;
+ }
+
+ submitFeedback(data: dto.SubmitFeedbackDto) {
+ return this.send('submitFeedback', data) as Promise;
+ }
+
+ requestFeedbackData() {
+ return this.send('requestFeedbackData', {}) as Promise;
+ }
+
requestGroupData(data: dto.RequestGroupDataDto) {
return this.send('requestGroupData', data) as Promise;
}
@@ -137,6 +221,14 @@ export class ServerApi {
return this.send('updateFcmToken', data) as Promise;
}
+ sendNotification(data: dto.SendNotificationDto) {
+ return this.send('sendNotification', data) as Promise;
+ }
+
+ removeFcmToken() {
+ return this.send('removeFcmToken', {}) as Promise;
+ }
+
requestOrganizationData(data: dto.RequestOrganizationDataDto) {
return this.send('requestOrganizationData', data) as Promise<
number | undefined
@@ -314,6 +406,11 @@ export class ServerApi {
this.socket.on('updateBearItemsData', data => callback(data));
}
+ onUpdateBearItemData(callback: (data: dto.UpdateBearItemDataDto) => void) {
+ this.socket.removeAllListeners('updateBearItemData');
+ this.socket.on('updateBearItemData', data => callback(data));
+ }
+
onUpdateUserInventoryData(
callback: (data: dto.UpdateUserInventoryDataDto) => void,
) {
@@ -386,4 +483,21 @@ export class ServerApi {
this.socket.removeAllListeners('quizProgress');
this.socket.on('quizProgress', data => callback(data));
}
+
+ onUpdateFeedbackData(callback: (data: dto.UpdateFeedbackDataDto) => void) {
+ this.socket.removeAllListeners('updateFeedbackData');
+ this.socket.on('updateFeedbackData', data => callback(data));
+ }
+
+ onUpdateCampusEventData(
+ callback: (data: dto.UpdateCampusEventDataDto) => void,
+ ) {
+ this.socket.removeAllListeners('updateCampusEventData');
+ this.socket.on('updateCampusEventData', data => callback(data));
+ }
+
+ onCampusEventList(callback: (data: dto.CampusEventListResponseDto) => void) {
+ this.socket.removeAllListeners('campusEventList');
+ this.socket.on('campusEventList', data => callback(data));
+ }
}
diff --git a/admin/src/components/ServerData.tsx b/admin/src/components/ServerData.tsx
index daef87c3..077dd9eb 100644
--- a/admin/src/components/ServerData.tsx
+++ b/admin/src/components/ServerData.tsx
@@ -7,6 +7,7 @@ import {
useState,
} from 'react';
import {
+ AdminBearItemDto,
ChallengeDto,
UpdateErrorDto,
EventDto,
@@ -29,6 +30,7 @@ const defaultData = {
users: new Map(),
groups: new Map(),
quizQuestions: new Map(),
+ bearItems: new Map(),
selectedEvent: '' as string,
selectedOrg: '' as string,
errors: new Map(),
@@ -93,6 +95,17 @@ const defaultData = {
async requestQuizQuestions(challengeId: string): Promise {
return undefined;
},
+ async updateBearItem(
+ bearItem: AdminBearItemDto,
+ ): Promise {
+ return undefined;
+ },
+ async deleteBearItem(id: string): Promise {
+ return undefined;
+ },
+ async requestAllBearItems(): Promise {
+ return undefined;
+ },
};
export const ServerDataContext = createContext(defaultData);
@@ -206,6 +219,18 @@ export function ServerDataProvider(props: { children: ReactNode }) {
requestQuizQuestions(challengeId: string) {
return sock.requestQuizQuestions({ challengeId });
},
+ updateBearItem(bearItem: AdminBearItemDto) {
+ return sock.updateBearItemData({ bearItem, deleted: false });
+ },
+ deleteBearItem(id: string) {
+ return sock.updateBearItemData({
+ bearItem: { id },
+ deleted: true,
+ });
+ },
+ requestAllBearItems() {
+ return sock.requestAllBearItems({});
+ },
}),
[sock],
);
@@ -214,6 +239,7 @@ export function ServerDataProvider(props: { children: ReactNode }) {
sock.send('requestOrganizationData', { admin: true });
sock.requestAllUserData({});
sock.requestGroupData({});
+ sock.requestAllBearItems({});
}, [sock]);
/** Update defaultData object when ServerApi websocket receives a response */
@@ -350,6 +376,20 @@ export function ServerDataProvider(props: { children: ReactNode }) {
return { ...prev, quizQuestions: newQuizQuestions };
});
});
+ sock.onUpdateBearItemData(data => {
+ setServerData(prev => {
+ const newBearItems = new Map(prev.bearItems);
+ if (data.deleted) {
+ newBearItems.delete(data.bearItem.id);
+ } else {
+ newBearItems.set(
+ (data.bearItem as AdminBearItemDto).id,
+ data.bearItem as AdminBearItemDto,
+ );
+ }
+ return { ...prev, bearItems: newBearItems };
+ });
+ });
}, [sock]);
if (!connection.connection) return <>{props.children}>;
diff --git a/game/android/build.gradle b/game/android/build.gradle
index fb2577b7..2387ccac 100644
--- a/game/android/build.gradle
+++ b/game/android/build.gradle
@@ -34,19 +34,6 @@ subprojects { subproject ->
subproject.evaluationDependsOn(':app')
}
-subprojects { project ->
- def disableExtract = {
- project.tasks.matching { it.name.contains('extractDebugAnnotations') }.configureEach {
- enabled = false
- }
- }
- try {
- project.afterEvaluate(disableExtract)
- } catch (Exception ignored) {
- disableExtract()
- }
-}
-
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
diff --git a/game/assets/buildabear/eyes/><.png b/game/assets/buildabear/eyes/angle_brackets.png
similarity index 100%
rename from game/assets/buildabear/eyes/><.png
rename to game/assets/buildabear/eyes/angle_brackets.png
diff --git a/game/assets/buildabear/eyes/squinty_eyes.png b/game/assets/buildabear/eyes/squinty_eyes.png
new file mode 100644
index 00000000..89d10de5
Binary files /dev/null and b/game/assets/buildabear/eyes/squinty_eyes.png differ
diff --git a/game/ios/Podfile.lock b/game/ios/Podfile.lock
index 76700fce..cd48c01c 100644
--- a/game/ios/Podfile.lock
+++ b/game/ios/Podfile.lock
@@ -270,7 +270,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
- device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
+ device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707
diff --git a/game/lib/api/game_client_api.dart b/game/lib/api/game_client_api.dart
index 99a14b22..890905c1 100644
--- a/game/lib/api/game_client_api.dart
+++ b/game/lib/api/game_client_api.dart
@@ -73,6 +73,11 @@ class GameClientApi {
Stream get updateBearItemsDataStream =>
_updateBearItemsDataController.stream;
+ final _updateBearItemDataController =
+ StreamController.broadcast(sync: true);
+ Stream get updateBearItemDataStream =>
+ _updateBearItemDataController.stream;
+
final _updateUserInventoryDataController =
StreamController.broadcast(sync: true);
Stream get updateUserInventoryDataStream =>
@@ -136,6 +141,21 @@ class GameClientApi {
Stream get quizProgressStream =>
_quizProgressController.stream;
+ final _updateFeedbackDataController =
+ StreamController.broadcast(sync: true);
+ Stream get updateFeedbackDataStream =>
+ _updateFeedbackDataController.stream;
+
+ final _updateCampusEventDataController =
+ StreamController.broadcast(sync: true);
+ Stream get updateCampusEventDataStream =>
+ _updateCampusEventDataController.stream;
+
+ final _campusEventListController =
+ StreamController.broadcast(sync: true);
+ Stream get campusEventListStream =>
+ _campusEventListController.stream;
+
final _reconnectedController = StreamController.broadcast(sync: true);
Stream get reconnectedStream => _reconnectedController.stream;
@@ -218,6 +238,11 @@ class GameClientApi {
(data) => _updateBearItemsDataController
.add(UpdateBearItemsDataDto.fromJson(data)));
+ sock.on(
+ "updateBearItemData",
+ (data) => _updateBearItemDataController
+ .add(UpdateBearItemDataDto.fromJson(data)));
+
sock.on(
"updateUserInventoryData",
(data) => _updateUserInventoryDataController
@@ -271,6 +296,21 @@ class GameClientApi {
sock.on("quizProgress",
(data) => _quizProgressController.add(QuizProgressDto.fromJson(data)));
+ sock.on(
+ "updateFeedbackData",
+ (data) => _updateFeedbackDataController
+ .add(UpdateFeedbackDataDto.fromJson(data)));
+
+ sock.on(
+ "updateCampusEventData",
+ (data) => _updateCampusEventDataController
+ .add(UpdateCampusEventDataDto.fromJson(data)));
+
+ sock.on(
+ "campusEventList",
+ (data) => _campusEventListController
+ .add(CampusEventListResponseDto.fromJson(data)));
+
_connectedController.add(true);
}
diff --git a/game/lib/api/game_client_dto.dart b/game/lib/api/game_client_dto.dart
index 572e9fa3..ae0a9ba7 100644
--- a/game/lib/api/game_client_dto.dart
+++ b/game/lib/api/game_client_dto.dart
@@ -29,6 +29,90 @@ enum BearSlotDto {
ACCESSORY,
}
+enum CampusEventCategoryDto {
+ SOCIAL,
+ CULTURAL,
+ ATHLETIC,
+ WELLNESS,
+ ACADEMIC,
+ ARTS,
+ CAREER,
+ COMMUNITY,
+ OTHER,
+}
+
+enum EventSourceDto {
+ API_EVENTS,
+ ADMIN_CREATED,
+ COMMUNITY_SUBMITTED,
+}
+
+enum CheckInMethodDto {
+ LOCATION,
+ QR_CODE,
+ EITHER,
+}
+
+enum CampusEventCategoriesDto {
+ SOCIAL,
+ CULTURAL,
+ ATHLETIC,
+ WELLNESS,
+ ACADEMIC,
+ ARTS,
+ CAREER,
+ COMMUNITY,
+ OTHER,
+}
+
+enum CampusEventSourceDto {
+ API_EVENTS,
+ ADMIN_CREATED,
+ COMMUNITY_SUBMITTED,
+}
+
+enum CampusEventCheckInMethodDto {
+ LOCATION,
+ QR_CODE,
+ EITHER,
+}
+
+enum RequestCampusEventsCategoriesDto {
+ SOCIAL,
+ CULTURAL,
+ ATHLETIC,
+ WELLNESS,
+ ACADEMIC,
+ ARTS,
+ CAREER,
+ COMMUNITY,
+ OTHER,
+}
+
+enum UpsertCampusEventCategoriesDto {
+ SOCIAL,
+ CULTURAL,
+ ATHLETIC,
+ WELLNESS,
+ ACADEMIC,
+ ARTS,
+ CAREER,
+ COMMUNITY,
+ OTHER,
+}
+
+enum UpsertCampusEventSourceDto {
+ API_EVENTS,
+ ADMIN_CREATED,
+ COMMUNITY_SUBMITTED,
+}
+
+enum UpsertCampusEventCheckInMethodDto {
+ LOCATION,
+ QR_CODE,
+ EITHER,
+}
+
enum ChallengeLocationDto {
ENG_QUAD,
ARTS_QUAD,
@@ -43,13 +127,45 @@ enum ChallengeLocationDto {
ANY,
}
+enum CheckInResultCheckInMethodDto {
+ LOCATION,
+ QR_CODE,
+}
+
+enum CheckInErrorCodeDto {
+ EVENT_NOT_FOUND,
+ EVENT_NOT_ACTIVE,
+ EVENT_NOT_APPROVED,
+ ALREADY_CHECKED_IN,
+ OUT_OF_RADIUS,
+ METHOD_NOT_ALLOWED,
+ INVALID_QR_CODE,
+ UNKNOWN_ERROR,
+}
+
+enum ClubSubmissionCategoryDto {
+ SOCIAL,
+ CULTURAL,
+ ATHLETIC,
+ WELLNESS,
+ ACADEMIC,
+ ARTS,
+ CAREER,
+ COMMUNITY,
+ OTHER,
+}
+
enum EventCategoryDto {
FOOD,
NATURE,
HISTORICAL,
- CAFE,
- DININGHALL,
- DORM,
+ RESIDENTIAL,
+ LANDMARK,
+ ARTS,
+ ATHLETICS,
+ LIBRARY,
+ ACADEMIC,
+ RECREATION,
}
enum EventTimeLimitationDto {
@@ -63,6 +179,12 @@ enum EventDifficultyDto {
Hard,
}
+enum FeedbackCategoryDto {
+ BUG_REPORT,
+ SUGGESTION,
+ GENERAL,
+}
+
enum QuizErrorCodeDto {
NO_QUESTIONS,
ALREADY_ANSWERED,
@@ -828,267 +950,1137 @@ class UpdatePurchaseResultDto {
late String itemId;
}
-class CompletedChallengeDto {
- Map toJson() {
- Map fields = {};
- return fields;
- }
-
- CompletedChallengeDto.fromJson(Map fields) {}
-
- void partialUpdate(CompletedChallengeDto other) {}
-
- CompletedChallengeDto();
-}
-
-class ChallengeDto {
+class AdminBearItemDto {
Map toJson() {
Map fields = {};
fields['id'] = id;
if (name != null) {
fields['name'] = name;
}
- if (location != null) {
- fields['location'] = location!.name;
- }
- if (description != null) {
- fields['description'] = description;
- }
- if (points != null) {
- fields['points'] = points;
- }
- if (imageUrl != null) {
- fields['imageUrl'] = imageUrl;
- }
- if (latF != null) {
- fields['latF'] = latF;
+ if (slot != null) {
+ fields['slot'] = slot!.name;
}
- if (longF != null) {
- fields['longF'] = longF;
+ if (cost != null) {
+ fields['cost'] = cost;
}
- if (awardingRadiusF != null) {
- fields['awardingRadiusF'] = awardingRadiusF;
+ if (assetKey != null) {
+ fields['assetKey'] = assetKey;
}
- if (closeRadiusF != null) {
- fields['closeRadiusF'] = closeRadiusF;
+ if (mimeType != null) {
+ fields['mimeType'] = mimeType;
}
- if (linkedEventId != null) {
- fields['linkedEventId'] = linkedEventId;
+ if (zIndex != null) {
+ fields['zIndex'] = zIndex;
}
- if (timerLength != null) {
- fields['timerLength'] = timerLength;
+ if (isDefault != null) {
+ fields['isDefault'] = isDefault;
}
return fields;
}
- ChallengeDto.fromJson(Map fields) {
+ AdminBearItemDto.fromJson(Map fields) {
id = fields["id"];
name = fields.containsKey('name') ? (fields["name"]) : null;
- location = fields.containsKey('location')
- ? (ChallengeLocationDto.values.byName(fields['location']))
- : null;
- description =
- fields.containsKey('description') ? (fields["description"]) : null;
- points = fields.containsKey('points') ? (fields["points"]) : null;
- imageUrl = fields.containsKey('imageUrl') ? (fields["imageUrl"]) : null;
- latF = fields.containsKey('latF') ? (fields["latF"]!.toDouble()) : null;
- longF = fields.containsKey('longF') ? (fields["longF"]!.toDouble()) : null;
- awardingRadiusF = fields.containsKey('awardingRadiusF')
- ? (fields["awardingRadiusF"]!.toDouble())
- : null;
- closeRadiusF = fields.containsKey('closeRadiusF')
- ? (fields["closeRadiusF"]!.toDouble())
+ slot = fields.containsKey('slot')
+ ? (BearSlotDto.values.byName(fields['slot']))
: null;
- linkedEventId =
- fields.containsKey('linkedEventId') ? (fields["linkedEventId"]) : null;
- timerLength =
- fields.containsKey('timerLength') ? (fields["timerLength"]) : null;
+ cost = fields.containsKey('cost') ? (fields["cost"]) : null;
+ assetKey = fields.containsKey('assetKey') ? (fields["assetKey"]) : null;
+ mimeType = fields.containsKey('mimeType') ? (fields["mimeType"]) : null;
+ zIndex = fields.containsKey('zIndex') ? (fields["zIndex"]) : null;
+ isDefault = fields.containsKey('isDefault') ? (fields["isDefault"]) : null;
}
- void partialUpdate(ChallengeDto other) {
+ void partialUpdate(AdminBearItemDto other) {
id = other.id;
name = other.name == null ? name : other.name;
- location = other.location == null ? location : other.location;
- description = other.description == null ? description : other.description;
- points = other.points == null ? points : other.points;
- imageUrl = other.imageUrl == null ? imageUrl : other.imageUrl;
- latF = other.latF == null ? latF : other.latF;
- longF = other.longF == null ? longF : other.longF;
- awardingRadiusF =
- other.awardingRadiusF == null ? awardingRadiusF : other.awardingRadiusF;
- closeRadiusF =
- other.closeRadiusF == null ? closeRadiusF : other.closeRadiusF;
- linkedEventId =
- other.linkedEventId == null ? linkedEventId : other.linkedEventId;
- timerLength = other.timerLength == null ? timerLength : other.timerLength;
+ slot = other.slot == null ? slot : other.slot;
+ cost = other.cost == null ? cost : other.cost;
+ assetKey = other.assetKey == null ? assetKey : other.assetKey;
+ mimeType = other.mimeType == null ? mimeType : other.mimeType;
+ zIndex = other.zIndex == null ? zIndex : other.zIndex;
+ isDefault = other.isDefault == null ? isDefault : other.isDefault;
}
- ChallengeDto({
+ AdminBearItemDto({
required this.id,
this.name,
- this.location,
- this.description,
- this.points,
- this.imageUrl,
- this.latF,
- this.longF,
- this.awardingRadiusF,
- this.closeRadiusF,
- this.linkedEventId,
- this.timerLength,
+ this.slot,
+ this.cost,
+ this.assetKey,
+ this.mimeType,
+ this.zIndex,
+ this.isDefault,
});
late String id;
late String? name;
- late ChallengeLocationDto? location;
- late String? description;
- late int? points;
- late String? imageUrl;
- late double? latF;
- late double? longF;
- late double? awardingRadiusF;
- late double? closeRadiusF;
- late String? linkedEventId;
- late int? timerLength;
+ late BearSlotDto? slot;
+ late int? cost;
+ late String? assetKey;
+ late String? mimeType;
+ late int? zIndex;
+ late bool? isDefault;
}
-class RequestChallengeDataDto {
+class UpdateBearItemDataDto {
Map toJson() {
Map fields = {};
- fields['challenges'] = challenges;
+ fields['bearItem'] = bearItem!.toJson();
+ fields['deleted'] = deleted;
return fields;
}
- RequestChallengeDataDto.fromJson(Map fields) {
- challenges = List.from(fields['challenges']);
+ UpdateBearItemDataDto.fromJson(Map fields) {
+ bearItem = AdminBearItemDto.fromJson(fields['bearItem']);
+ deleted = fields["deleted"];
}
- void partialUpdate(RequestChallengeDataDto other) {
- challenges = other.challenges;
+ void partialUpdate(UpdateBearItemDataDto other) {
+ bearItem = other.bearItem;
+ deleted = other.deleted;
}
- RequestChallengeDataDto({
- required this.challenges,
+ UpdateBearItemDataDto({
+ required this.bearItem,
+ required this.deleted,
});
- late List challenges;
+ late AdminBearItemDto bearItem;
+ late bool deleted;
}
-class UpdateChallengeDataDto {
+class RequestAllBearItemsDto {
Map toJson() {
Map fields = {};
- fields['challenge'] = challenge!.toJson();
- fields['deleted'] = deleted;
return fields;
}
- UpdateChallengeDataDto.fromJson(Map fields) {
- challenge = ChallengeDto.fromJson(fields['challenge']);
- deleted = fields["deleted"];
- }
-
- void partialUpdate(UpdateChallengeDataDto other) {
- challenge = other.challenge;
- deleted = other.deleted;
- }
+ RequestAllBearItemsDto.fromJson(Map fields) {}
- UpdateChallengeDataDto({
- required this.challenge,
- required this.deleted,
- });
+ void partialUpdate(RequestAllBearItemsDto other) {}
- late ChallengeDto challenge;
- late bool deleted;
+ RequestAllBearItemsDto();
}
-class RequestEventTrackerDataDto {
+class CampusEventDto {
Map toJson() {
Map fields = {};
- fields['trackedEvents'] = trackedEvents;
+ fields['id'] = id;
+ fields['title'] = title;
+ fields['description'] = description;
+ if (imageUrl != null) {
+ fields['imageUrl'] = imageUrl;
+ }
+ fields['startTime'] = startTime;
+ fields['endTime'] = endTime;
+ fields['allDay'] = allDay;
+ fields['locationName'] = locationName;
+ if (address != null) {
+ fields['address'] = address;
+ }
+ fields['latitude'] = latitude;
+ fields['longitude'] = longitude;
+ fields['categories'] =
+ categories!.map((dynamic val) => val!.name).toList();
+ fields['tags'] = tags;
+ fields['source'] = source!.name;
+ if (externalUrl != null) {
+ fields['externalUrl'] = externalUrl;
+ }
+ if (organizerName != null) {
+ fields['organizerName'] = organizerName;
+ }
+ if (registrationUrl != null) {
+ fields['registrationUrl'] = registrationUrl;
+ }
+ fields['checkInMethod'] = checkInMethod!.name;
+ fields['pointsForAttendance'] = pointsForAttendance;
+ fields['featured'] = featured;
+ fields['attendanceCount'] = attendanceCount;
+ fields['rsvpCount'] = rsvpCount;
return fields;
}
- RequestEventTrackerDataDto.fromJson(Map fields) {
- trackedEvents = List.from(fields['trackedEvents']);
- }
-
- void partialUpdate(RequestEventTrackerDataDto other) {
- trackedEvents = other.trackedEvents;
+ CampusEventDto.fromJson(Map fields) {
+ id = fields["id"];
+ title = fields["title"];
+ description = fields["description"];
+ imageUrl = fields.containsKey('imageUrl') ? (fields["imageUrl"]) : null;
+ startTime = fields["startTime"];
+ endTime = fields["endTime"];
+ allDay = fields["allDay"];
+ locationName = fields["locationName"];
+ address = fields.containsKey('address') ? (fields["address"]) : null;
+ latitude = fields["latitude"];
+ longitude = fields["longitude"];
+ categories = fields["categories"]
+ .map(
+ (dynamic val) => CampusEventCategoriesDto.values.byName(val))
+ .toList();
+ tags = List.from(fields['tags']);
+ source = CampusEventSourceDto.values.byName(fields['source']);
+ externalUrl =
+ fields.containsKey('externalUrl') ? (fields["externalUrl"]) : null;
+ organizerName =
+ fields.containsKey('organizerName') ? (fields["organizerName"]) : null;
+ registrationUrl = fields.containsKey('registrationUrl')
+ ? (fields["registrationUrl"])
+ : null;
+ checkInMethod =
+ CampusEventCheckInMethodDto.values.byName(fields['checkInMethod']);
+ pointsForAttendance = fields["pointsForAttendance"];
+ featured = fields["featured"];
+ attendanceCount = fields["attendanceCount"];
+ rsvpCount = fields["rsvpCount"];
}
- RequestEventTrackerDataDto({
- required this.trackedEvents,
+ void partialUpdate(CampusEventDto other) {
+ id = other.id;
+ title = other.title;
+ description = other.description;
+ imageUrl = other.imageUrl == null ? imageUrl : other.imageUrl;
+ startTime = other.startTime;
+ endTime = other.endTime;
+ allDay = other.allDay;
+ locationName = other.locationName;
+ address = other.address == null ? address : other.address;
+ latitude = other.latitude;
+ longitude = other.longitude;
+ categories = other.categories;
+ tags = other.tags;
+ source = other.source;
+ externalUrl = other.externalUrl == null ? externalUrl : other.externalUrl;
+ organizerName =
+ other.organizerName == null ? organizerName : other.organizerName;
+ registrationUrl =
+ other.registrationUrl == null ? registrationUrl : other.registrationUrl;
+ checkInMethod = other.checkInMethod;
+ pointsForAttendance = other.pointsForAttendance;
+ featured = other.featured;
+ attendanceCount = other.attendanceCount;
+ rsvpCount = other.rsvpCount;
+ }
+
+ CampusEventDto({
+ required this.id,
+ required this.title,
+ required this.description,
+ this.imageUrl,
+ required this.startTime,
+ required this.endTime,
+ required this.allDay,
+ required this.locationName,
+ this.address,
+ required this.latitude,
+ required this.longitude,
+ required this.categories,
+ required this.tags,
+ required this.source,
+ this.externalUrl,
+ this.organizerName,
+ this.registrationUrl,
+ required this.checkInMethod,
+ required this.pointsForAttendance,
+ required this.featured,
+ required this.attendanceCount,
+ required this.rsvpCount,
});
- late List trackedEvents;
+ late String id;
+ late String title;
+ late String description;
+ late String? imageUrl;
+ late String startTime;
+ late String endTime;
+ late bool allDay;
+ late String locationName;
+ late String? address;
+ late int latitude;
+ late int longitude;
+ late List categories;
+ late List tags;
+ late CampusEventSourceDto source;
+ late String? externalUrl;
+ late String? organizerName;
+ late String? registrationUrl;
+ late CampusEventCheckInMethodDto checkInMethod;
+ late int pointsForAttendance;
+ late bool featured;
+ late int attendanceCount;
+ late int rsvpCount;
}
-class SetCurrentChallengeDto {
+class RequestCampusEventsDto {
Map toJson() {
Map fields = {};
- fields['challengeId'] = challengeId;
+ fields['page'] = page;
+ fields['limit'] = limit;
+ if (dateFrom != null) {
+ fields['dateFrom'] = dateFrom;
+ }
+ if (dateTo != null) {
+ fields['dateTo'] = dateTo;
+ }
+ if (categories != null) {
+ fields['categories'] =
+ categories!.map((dynamic val) => val!.name).toList();
+ }
+ if (search != null) {
+ fields['search'] = search;
+ }
+ if (featured != null) {
+ fields['featured'] = featured;
+ }
return fields;
}
- SetCurrentChallengeDto.fromJson(Map fields) {
- challengeId = fields["challengeId"];
+ RequestCampusEventsDto.fromJson(Map fields) {
+ page = fields["page"];
+ limit = fields["limit"];
+ dateFrom = fields.containsKey('dateFrom') ? (fields["dateFrom"]) : null;
+ dateTo = fields.containsKey('dateTo') ? (fields["dateTo"]) : null;
+ categories = fields.containsKey('categories')
+ ? (fields["categories"]
+ .map((dynamic val) =>
+ RequestCampusEventsCategoriesDto.values.byName(val))
+ .toList())
+ : null;
+ search = fields.containsKey('search') ? (fields["search"]) : null;
+ featured = fields.containsKey('featured') ? (fields["featured"]) : null;
}
- void partialUpdate(SetCurrentChallengeDto other) {
- challengeId = other.challengeId;
+ void partialUpdate(RequestCampusEventsDto other) {
+ page = other.page;
+ limit = other.limit;
+ dateFrom = other.dateFrom == null ? dateFrom : other.dateFrom;
+ dateTo = other.dateTo == null ? dateTo : other.dateTo;
+ categories = other.categories == null ? categories : other.categories;
+ search = other.search == null ? search : other.search;
+ featured = other.featured == null ? featured : other.featured;
}
- SetCurrentChallengeDto({
- required this.challengeId,
+ RequestCampusEventsDto({
+ required this.page,
+ required this.limit,
+ this.dateFrom,
+ this.dateTo,
+ this.categories,
+ this.search,
+ this.featured,
});
- late String challengeId;
+ late int page;
+ late int limit;
+ late String? dateFrom;
+ late String? dateTo;
+ late List? categories;
+ late String? search;
+ late bool? featured;
}
-class RequestAvailableChallengesDto {
+class CampusEventListDto {
Map toJson() {
Map fields = {};
+ fields['events'] = events!
+ .map