Skip to content
Open
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
19 changes: 19 additions & 0 deletions lego-webapp/app/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,22 @@ export type EventAdministrate = Omit<Event, 'createdBy' | 'comments'> & {
createdBy: number;
comments: number[];
};

export type Country = {
id: number;
name: string;
};

export type University = {
id: EntityId;
name: string;
country: string;
}

export type Exchange = {
id: number;
student: PublicUser;
university: University;
semester: 'vår' | 'høst';
year: number;
};
14 changes: 13 additions & 1 deletion lego-webapp/pages/users/@username/+Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ import { EmailLists } from '~/pages/users/@username/_components/EmailLists';
import { GSuiteInfo } from '~/pages/users/@username/_components/GSuiteInfo';
import { GroupTree } from '~/pages/users/@username/_components/GroupTree';
import { Permissions } from '~/pages/users/@username/_components/Permissions';
import { UserInfo } from '~/pages/users/@username/_components/UserInfo';
import { Exchanges, ExchangesInfo, UserInfo } from '~/pages/users/@username/_components/UserInfo';
import { useIsCurrentUser } from '~/pages/users/utils';
import { fetchPrevious, fetchUpcoming } from '~/redux/actions/EventActions';
// import { fetchExchanges } from '~/redux/actions/ExchangeActions';
import { fetchAllWithType } from '~/redux/actions/GroupActions';
import { fetchUser } from '~/redux/actions/UserActions';
import { useAppDispatch, useAppSelector } from '~/redux/hooks';
import { EntityType } from '~/redux/models/entities';
import { useCurrentUser } from '~/redux/slices/auth';
import { selectAllEvents } from '~/redux/slices/events';
import { selectAllExchanges } from '~/redux/slices/exchanges';
import { selectGroupsByType, selectGroupEntities } from '~/redux/slices/groups';
import { selectPaginationNext } from '~/redux/slices/selectors';
import { selectUserByUsername } from '~/redux/slices/users';
Expand All @@ -48,6 +50,9 @@ import type { ExclusifyUnion } from 'app/types';
import type { ListEvent } from '~/redux/models/Event';
import type { PublicGroup } from '~/redux/models/Group';
import type { CurrentUser, PublicUserWithGroups } from '~/redux/models/User';
import { query } from 'express';
import { selectEmojisByIds } from '~/redux/slices/emojis';
import { useEffect } from 'react';

const UserProfile = () => {
const params = useParams<{ username: string }>();
Expand Down Expand Up @@ -106,6 +111,9 @@ const UserProfile = () => {

const groupEntities = useAppSelector(selectGroupEntities);

const emojis = useAppSelector((state) =>
selectEmojisByIds(state, [':japan:'])
)
const dispatch = useAppDispatch();

usePreparedEffect(
Expand Down Expand Up @@ -137,6 +145,7 @@ const UserProfile = () => {
abakusEmailLists = [],
permissionsPerGroup = [],
photoConsents,
exchanges = [],
} = user;

const allAbakusGroupsWithPerms = uniqBy(
Expand Down Expand Up @@ -232,6 +241,9 @@ const UserProfile = () => {
<div className={styles.info}>
<UserInfo user={user} />

{exchanges?.length > 0 &&
<ExchangesInfo exchanges={exchanges} />
}
{showSettings && <Penalties userId={user.id} />}

{showSettings && photoConsents && photoConsents.length > 0 && (
Expand Down
125 changes: 125 additions & 0 deletions lego-webapp/pages/users/@username/_components/ExchangeField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Button, ConfirmModal, Flex, Icon } from '@webkom/lego-bricks';
import { debounce } from 'lodash-es';
import { GraduationCap, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { Field } from 'react-final-form';
import { FieldArray } from 'react-final-form-arrays';
import type { University } from '~/app/models';
import { SelectInput } from '~/components/Form';
import { autocomplete } from '~/redux/actions/SearchActions';
import { useAppDispatch, useAppSelector } from '~/redux/hooks';
import { selectAutocompleteRedux } from '~/redux/slices/search';

const ExchangeField = () => {
const [query, setQuery] = useState('');
const searchResults = useAppSelector(selectAutocompleteRedux);
const dispatch = useAppDispatch();

const currentYear = new Date().getFullYear();
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
label: String(currentYear - 5 + i),
value: currentYear - 5 + i,
}));

const semesterOptions = [
{ label: 'Vår', value: 'vår' },
{ label: 'Høst', value: 'høst' },
];

const onSearchUniversities = debounce((searchQuery: string) => {
setQuery(searchQuery);
if (searchQuery.length >= 2) {
dispatch(autocomplete(searchQuery, ['users.university']));
}
}, 300);

// Filter and map search results to university options
const universityOptions = searchResults
.filter((result) => result.contentType === 'users.university')
.map((result) => {
const uni = result as unknown as University;
return {
label: `${uni.name}, ${uni.country.name}`,
value: uni.id,
};
});

return (
<FieldArray name="exchanges">
{({ fields }) => (
<Flex column gap="var(--spacing-md)">
<Flex alignItems="center" gap="var(--spacing-sm)">
<Icon iconNode={<GraduationCap />} />
<label>Utvekslingsopphold</label>
</Flex>

{fields.map((name, index) => (
<Flex
key={name}
gap="var(--spacing-sm)"
alignItems="flex-end"
wrap
>
<div style={{ flex: '2 1 300px', minWidth: '200px' }}>
<Field
name={`${name}.university`}
label={index === 0 ? 'Universitet' : undefined}
component={SelectInput.AutocompleteField}
onInputChange={onSearchUniversities}
options={universityOptions}
placeholder="Søk etter universitet..."
required
/>
</div>

<div style={{ flex: '1 1 120px', minWidth: '100px' }}>
<Field
name={`${name}.semester`}
label={index === 0 ? 'Semester' : undefined}
component={SelectInput.Field}
options={semesterOptions}
placeholder="Velg semester"
required
/>
</div>

<div style={{ flex: '1 1 120px', minWidth: '100px' }}>
<Field
name={`${name}.year`}
label={index === 0 ? 'År' : undefined}
component={SelectInput.Field}
options={yearOptions}
placeholder="Velg år"
required
/>
</div>

<ConfirmModal
title="Fjern utveksling"
message="Er du sikker på at du vil fjerne denne utvekslingen?"
onConfirm={() => fields.remove(index)}
>
{({ openConfirmModal }) => (
<Button onPress={openConfirmModal} danger size="small">
<Trash2 size={16} />
</Button>
)}
</ConfirmModal>
</Flex>
))}

<Button
onPress={() =>
fields.push({ university: null, semester: null, year: null })
}
secondary
>
Legg til utveksling
</Button>
</Flex>
)}
</FieldArray>
);
};

export default ExchangeField;
23 changes: 23 additions & 0 deletions lego-webapp/pages/users/@username/_components/UserInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Flex, Icon } from '@webkom/lego-bricks';
import Emoji from '~/components/Emoji';
import Pill from '~/components/Pill';
import {
EmailInfoField,
InfoField,
ProfileSection,
} from '~/pages/users/@username/_components/ProfileSection';
import styles from '~/pages/users/@username/_components/UserProfile.module.css';
import { Exchange } from '~/redux/models/Exchange';
import type { CurrentUser, PublicUserWithGroups } from '~/redux/models/User';

const GithubField = ({ githubUsername }: { githubUsername: string }) => (
Expand Down Expand Up @@ -55,3 +58,23 @@ export const UserInfo = ({ user }: Props) => (
</Flex>
</ProfileSection>
);

export const ExchangesInfo = ({ exchanges }: { exchanges: Exchange[] }) => (
// <ProfileSection title="Utvekslingsopphold">
// {exchanges.map((exchange) => <ExchangePill key={exchange.id} exchange={exchange} />)}
// </ProfileSection >
<>
<h3>Utvekslingsopphold</h3>
<> {exchanges.map((exchange) => <ExchangePill key={exchange.id} exchange={exchange} />)}
</>
</>
);

export const ExchangePill = ({ exchange }: { exchange: Exchange }) => (
<Flex gap="var(--spacing-sm">
<Pill color={exchange.semester == 'vår' ? 'var(--success-color)' : 'peru'}>{exchange.semester.slice(0, 1)}{exchange.year - 2000}</Pill>
<Pill key={exchange.id}>{exchange.university.name}</Pill>
{/* <Pill key={exchange.id}><Emoji unicodeString={exchange.university.country.emoji.unicodeString} /></Pill> */}

</Flex>
)
53 changes: 52 additions & 1 deletion lego-webapp/pages/users/@username/settings/profile/+Page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Accordion, Flex, Icon, LoadingIndicator } from '@webkom/lego-bricks';
import { usePreparedEffect } from '@webkom/react-prepare';
import arrayMutators from 'final-form-arrays';
import { ChevronRight, Github, Linkedin, Mail } from 'lucide-react';
import { Field } from 'react-final-form';

import { ContentMain } from '~/components/Content';
import {
Form,
Expand All @@ -12,6 +14,7 @@ import {
SubmitButton,
SubmissionError,
RowSection,
MultiSelectGroup,
} from '~/components/Form';
import ToggleSwitch from '~/components/Form/ToggleSwitch';
import { useIsCurrentUser } from '~/pages/users/utils';
Expand All @@ -38,6 +41,7 @@ import ThemeSelector from './ThemeSelector';
import UserImage from './UserImage';
import styles from './UserSettings.module.css';
import type { CurrentUser } from '~/redux/models/User';
import ExchangeField from '../../_components/ExchangeField';

type GenderKey = keyof typeof Gender;

Expand All @@ -53,6 +57,11 @@ type FormValues = {
isAbakusMember: boolean;
githubUsername?: string;
linkedinId?: string;
exchanges: Array<{
university: { label: string; value: number };
semester: 'V' | 'H';
year: number;
}>;
};

const TypedLegoForm = LegoFinalForm<FormValues>;
Expand Down Expand Up @@ -95,7 +104,14 @@ const UserSettings = () => {
const body = {
...values,
gender: values.gender.value,
};
exchanges: values.exchanges
?.filter((ex) => ex.university && ex.semester && ex.year)
.map((ex) => ({
university_id: ex.university!.value,
semester: ex.semester!,
year: ex.year!,
})) || [],
}

dispatch(updateUser(body));
};
Expand All @@ -114,6 +130,14 @@ const UserSettings = () => {
isAbakusMember: user.isAbakusMember,
githubUsername: user.githubUsername ?? '',
linkedinId: user.linkedinId ?? '',
exchanges: user.exchanges?.map((exchange) => ({
university: {
label: `${exchange.university.name}, ${exchange.university.country.name}`,
value: exchange.university.id,
},
semester: exchange.semester,
year: exchange.year,
})) || [],
};

return (
Expand All @@ -124,6 +148,8 @@ const UserSettings = () => {
validate={validate}
keepDirtyOnReinitialize
subscription={{}}
mutators={{ ...arrayMutators }}

>
{({ handleSubmit }) => (
<Form onSubmit={handleSubmit} className={styles.settings}>
Expand Down Expand Up @@ -229,6 +255,31 @@ const UserSettings = () => {
parse={(value) => value}
/>
</RowSection>

<ExchangeField />
<MultiSelectGroup legend="Fargetema" name="selectedTheme">
<Field
name="selectedTheme"
label="Auto"
value="auto"
type="radio"
component={RadioButton.Field}
/>
<Field
name="selectedTheme"
label="Lyst"
value="light"
type="radio"
component={RadioButton.Field}
/>
<Field
name="selectedTheme"
label="Mørkt"
value="dark"
type="radio"
component={RadioButton.Field}
/>
</MultiSelectGroup>
</Flex>

<ThemeSelector />
Expand Down
4 changes: 4 additions & 0 deletions lego-webapp/redux/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,7 @@ export const FeatureFlag = {
EDIT: generateStatuses('FeatureFlag.EDIT'),
DELETE: generateStatuses('FeatureFlag.DELETE'),
};

export const Exchange = {
FETCH_ALL: generateStatuses('Exchange.FETCH_ALL')
}
19 changes: 19 additions & 0 deletions lego-webapp/redux/actions/ExchangeActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Exchange } from '~/redux/actionTypes'
import callAPI from '~/redux/actions/callAPI';
import { exchangeSchema } from '~/redux/schemas';

export function fetchExchanges({ query, next = false }) {
return callAPI({
types: Exchange.FETCH_ALL,
endpoint: '/exchanges/',
schema: [exchangeSchema],
query,
pagination: {
fetchNext: next,
},
meta: {
errorMessage: 'Henting av utvekslingsopphold feilet',
},
propagateError: true,
});
}
Loading