diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 28ca3edb..238761a7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -78,6 +78,7 @@ model Corps { language String @default("sv") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + contactURL String? roles Role[] permissions Permission[] diff --git a/src/app/account/preferences.tsx b/src/app/account/preferences.tsx index 8a1691b4..484e6a18 100644 --- a/src/app/account/preferences.tsx +++ b/src/app/account/preferences.tsx @@ -24,6 +24,7 @@ const initialValues = { lactoseFree: false, otherFoodPrefs: '', email: '', + contactURL: '', mainInstrument: '', }; type FormValues = typeof initialValues; @@ -59,6 +60,7 @@ const AccountPreferences = () => { lactoseFree: corps.foodPrefs?.lactoseFree ?? false, otherFoodPrefs: corps.foodPrefs?.other ?? '', email: corps.user.email || undefined, + contactURL: corps.contactURL ?? '', mainInstrument, }); }, [corps]); @@ -86,6 +88,8 @@ const AccountPreferences = () => { label: i.instrument.name, })); + const isTrivselOmbud = corps?.roles.some(role => role.name === "Trivselombud"); + return (
@@ -155,6 +159,14 @@ const AccountPreferences = () => { label={lang('Pronomen', 'Pronouns')} {...form.getInputProps('pronouns')} /> + { + (isTrivselOmbud) && + () + } +

{lang('Matpreferenser', 'Food preferences')}

diff --git a/src/app/info/page.tsx b/src/app/info/page.tsx new file mode 100644 index 00000000..44f6ad01 --- /dev/null +++ b/src/app/info/page.tsx @@ -0,0 +1,305 @@ +import { lang } from 'utils/language'; +import PositionInfobox from 'components/corps/position-infobox'; +import { api } from 'trpc/server'; +import { IconMail } from '@tabler/icons-react'; +import ActionIcon from 'components/input/action-icon'; + + +const styrelseOrder: Dictionary = { + "Ordförande": 1, + "ViceOrdförande": 2, + "Sekreterare": 3, + "Kassör": 4, +}; + + +const Positions = async () => { + const roles = await api.permission.getRoles.query(); + const ordoredBoardCorps = await Promise.all( + roles + .filter(role => Object.keys(styrelseOrder).includes(role.name)) + .sort((a, b) => styrelseOrder[a.name] - styrelseOrder[b.name]) + .flatMap(role => + role.corpsii.map(async (corps) => { + const result = await api.corps.get.query({ id: corps.id }); + if (!result) throw new Error("Corps not found"); + return result; + }) + ) + ); + + const TrivselCorps = await Promise.all( + roles + .filter(role => role.name === "Trivselombud") + .flatMap(role => + role.corpsii.map(async (corps) => { + const result = await api.corps.get.query({ id: corps.id }); + if (!result) throw new Error("Corps not found"); + return result; + }) + ) + ); + + return ( +
+

{lang('Ansvarsposter', 'Positions of responsibility')}

+
+
+

{lang('Styrelsen', 'The Board')}

+ {lang(`Styrelsen leder Bleckhornen under ett verksamhetsår. Tillsammans med dirigenter, + balettledare, utskottsmedlemmar och andra förtroendevalda ansvarar styrelsen för planering + av repor, spelningar, sittningar, konserter, resor och andra aktiviteter. Detta görs med stöd och + samarbete med hela corpset`, + + `The board leads Bleckhornen throughout the year. Together with conductors, ballet leaders, + committee members, and other elected representatives, the board is responsible for planning + rehearsals, performances, formal sittings, concerts, trips, and other activities. This is done + with the support and collaboration of the entire corps.`)} +
+ {ordoredBoardCorps.map((corps) => ( +
+ {corps.roles.filter(role => Object.keys(styrelseOrder).includes(role.name))[0]?.name} + +
+ )) + } +
+
+
+ +
+
+

Trivselombud

+ {lang(`Trivselombuden i Bleckhornen har i uppdrag att bidra till trivsel och trygghet inom + föreningen. Till oss kan du komma om du känner att du har upplevt något inom Bleckhornen + som känns fel, eller om du känner att du behöver stöd eller prata om något. Trivselombuden + kan du alltid komma pch prata med när du känner för det, men du kan också kontakta oss via + våra formulär, där det även finns möjlighet att vara anonym.`, + + `The Wellbeing Representatives in Bleckhornen are responsible for promoting comfort and + safety within the association. You can come to us if you've experienced something within + Bleckhornen that feels wrong, or if you feel that you need support or just someone to talk to. + You can always approach the Wellbeing Representatives whenever you feel the need, but you + can also contact us through our forms, where there is also an option to remain anonymous.`)} + {roles.filter((role) => ( + role.name == "Trivselombud" + )).map((role) => ( +
+ {TrivselCorps.map((corps) => ( +
+ +
+ )) + } +
+ ))} +
+
+ +
+
+

Utskott

+ {lang(`I Bleckhornen finns flera utskott, där varje utskott har sina specifika uppgifter. + Tillsammans ser de till att föreningen fungerar smidigt och utvecklas. + Det finns ett utskott för alla, där din kreativitet och personlighet får flöda. + Samtidigt får du även chansen att lära känna personer från olika sektioner.`, + + `In Bleckhornen, there are several committees, each with its own specific tasks. + Together, they ensure that the association runs smoothly and continues to develop. + There’s a committee for everyone, where your creativity and personality can shine. + At the same time, you also get the chance to meet and get to know people from different sections.`)} + +
+

Notmarskeriet

+ + + +
+ {lang(`Notmarskeriet ansvarar för alla Bleckhornens noter. Detta innebär att vi skriver ut 📠, sätter + in 📒, och arkiverar ️ alla de noter som finns i våra pärmar och häften! Arbetsbördan som + notmarsk är generellt koncentrerad runt julkonserten och karnevalerna, och då kan den vara + rätt stor, men under den tidiga hösten och samt så gott som hela våren de år Lund ej gästas av + karneval är oftast relativt lite att göra.`, + + `The "Notmarskeri" (roughly "Note Marshallery") is responsible for Bleckhornen's sheet + music. This means that we print 📠, insert 📒, and archive ️ all the sheet music that can be + found in our folders and booklets! A note marshal's workload is typically concentrated + around the Christmas concert and the Karnevals, and at those points it can be a bit, but early + Autumn and basically all of the spring semester (on non-karneval years) are typically very + free`)} + +
+

Arkivet

+ + + +
+ {lang(`Arkivet är utskottet som förevigar allt skoj! Vi ser till att spara minnen från det roliga vi gör, + bland annat i form av bilder, filmer och affischer som corps har producerat. Vi sparar också + resultat av det slit andra funktionärer lägger ner, som corpset skulle kunna behöva i + framtiden.`, + + `The Archive is the committee that immortalizes all the fun! We make sure to preserve + memories from all the enjoyable things we do. For example, in the form of photos, videos, + and posters produced by the corps. We also keep the results of the hard work put in by other + functionaries, which the corps might need in the future.`)} + +
+

PR

+ + + +
+ {lang(`PR är utskottet som ser till att vi syns och hörs även utöver spelningarna! Vårt arbete är + främst att sköta våra sociala medier och fota på spelningar. Inför julkonserten gör vi även + affischerna, programbladen och söker spons. I detta utskott får kreativa idéer komma till liv!`, + + `PR is the committee that makes sure we’re seen and heard even beyond our performances! + Our work mainly involves managing our social media and taking photos at gigs. Before the + Christmas concert, we also create the posters and programs and handle sponsorships. In this + committee, creative ideas come to life!`)} + +
+

Baren

+ + + +
+ {lang(`Det är baren som förser corpset med grogg! (Ber du snällt kanske du till och med kan få en drink 😉) + Den första torsdagen varje månad bjuder vi in till extra festlig eftersits med extra festlig dryck! + Vi serverar dessutom fördrink inför corpsafton, och rattar Bussbaren hela vägen upp till SOF och STORK. + Eins, zwei, drei, gesoffa!`, + + `The Bar is the committee that keeps the corps supplied with drinks! (If you ask nicely, you might even get a cocktail 😉) + On the first Thursday of every month, we host an extra festive afterparty with extra festive beverages! + We also serve pre-drinks before corps evenings and run the Bus Bar all the way to SOF and STORK.`)} + +
+

Sexmästeriet

+ +
+ {lang(`Sexmästeriet är utskottet som ser till att ingen går hungrig! Vi lagar mat inför både Vårcorps och Höstcorps, + och ser till att hela corpset får njuta av god mat. + Tillsammans handlar vi ingredienser, lagar maten och har det riktigt roligt! + Som medlem i Sexmästeriet får du också vara med och bestämma vad som ska lagas (och presentera maten under middagarna).`, + + `The Culinary Committee makes sure no one goes hungry! + We cook for both Vårcorps and Höstcorps, making sure the whole corps gets to enjoy tasty food. + Together, we shop for ingredients, cook, and have a lot of fun along the way! + As a member, you also get to help decide what’s on the menu (and show off your creations at the dinners).`)} + +
+

ITK

+ + + +
+ {lang(`ITK har ansvar för drift av alla Bleckhornens hemsidor, samt vidareutveckling av Blindtarmen. + Driftansvaret includerar blindtarmen, den publika hemsidan och vår interna wiki.`, + + `ITK has responsibility for the operation of all Bleckhornens websites, as well as developing Blindtarmen. + The operational responsebility includes Blindtarmen, the public website, and our internal wiki`)} + +
+

Pryl & prov

+ + + +
+ {lang(`I pryl & prov har vi ansvar för corpsets merch och provelever! + Vi försöker se till att proveleverna känner sig välkomna i föreningen + och att de alltid har någon att rikta frågor till om föreningen. + Detta gör vi genom att anordna tillställningar som t.ex. provelevsfördrinkar och en provelevsdag! + När det kommer till merchen köper vi in och säljer föreningens merch, + och ibland när vi får feeling designar vi också ny merch!!`, + + `In Pryl & Prov, we’re responsible for the corps’ merch and for the newmembers! + We make sure that new members feel welcome in Bleckhornen + and that they always have someone to turn to with questions about how things work. + We do this by organizing events such as pre-drinks for the new members and a special probationary members’ day! + When it comes to merch, we handle the purchasing and sales of the orchestra's merchandise and sometimes, + when we’re feeling inspired, we even design new merch ourselves!`)} + +
+

Materialförvaltarna

+ + + +
+ {lang(`Materialförvaltarna tar hand om och utvecklar Tarmen och ser till Bleckhornens prylar fungerar. + Har du ett roligt projekt du skulle vilja genomföra kan du alltid dryfta din idé med oss för att få tips och stöd.`, + + `The materials managers take care of and develop Tarmen and make sure the Bleckhorns’ equipment works. + If you have a fun project you’d like to carry out, you can always discuss your idea with us to get tips and support.`)} + +
+

Medaljeriet

+ + + +
+ {lang(`Vi i Medaljeriet håller koll på vilka medaljer som ska köpas in och delas ut + och ger på så sätt corpsaftnarna och julkoncertsbanketten det där lilla extra! + Vi designar också de temaenliga julkoncertsmedaljerna varje år! + Utskottets finurliga tolkning av temat blir en fin souvenir till alla deltagande corps.`, + + `We in the Medal committee keep track of which medals are to be ordered and given out, + and thus we bring that extra shine to the dinner parties and the Christmas concert banquet! + We also design the Christmas concert medals in accordance with the concert's theme each year! + The committee's clever interpretation of the theme ends up as a nice souvenir for all participating corps.`)} + +
+ +

Import

+ +
+ {lang(`Vi i importen ser till att det finns den finaste ölen och cidern till ett överkomligt pris. + Därför åker vi på roadtrips över Öresund och med färjan över Fehmarnbältet för att köpa de bästa danska produkterna i Tyskland.`, + + `We from the import committee make sure with the finest beer and cider for an affordable prize. + Therefore we go on roadtrips across the Öresund and with the ferry over Fehmarn belt to buy the best Danish products in Germany.`)} + +
+

Export

+ +
+ {lang(`Vi i Exporten ser till att corpset aldrig går hungriga! + Vi fyller på med snacks, dryck och såklart billys! + Oavsett om det är rep, spelning, så ser vi till att corpset håller humöret på topp.`, + + `Exporten makes sure the corps never goes hungry! + We keep the snacks and drinks flowing and of course, plenty of Billy’s! + Whether it’s a rehearsal or a gig, we make sure the corps stays happy, energized, and ready to play.`)} + +
+
+
+ ); +}; + +export default Positions; + diff --git a/src/app/links/page.tsx b/src/app/links/page.tsx index f212ae0a..facbe356 100644 --- a/src/app/links/page.tsx +++ b/src/app/links/page.tsx @@ -49,6 +49,17 @@ const Links = () => { {lang('Notdriven', 'The Note Drive')} +
  • + + {lang( + 'Samply', + "Samply", + )} + +
  • { number: corps.number?.toString() || '', bNumber: corps.bNumber?.toString() || '', email: corps.user.email ?? '', + contactURL: corps.contactURL ?? '', mainInstrument, otherInstruments, roles: corps.roles.map((r) => r.name as Permission), @@ -133,6 +135,7 @@ const CorpsForm = ({ corpsId }: AdminCorpsProps) => {
  • +
    diff --git a/src/components/corps/infobox.tsx b/src/components/corps/infobox.tsx index a537b39e..38106839 100644 --- a/src/components/corps/infobox.tsx +++ b/src/components/corps/infobox.tsx @@ -86,7 +86,7 @@ const CorpsInfobox = ({ id, open }: CorpsInfoboxProps) => { const joinedAt = (firstGigDate?.getTime() ?? Number.MAX_VALUE) < - (firstRehearsalDate?.getTime() ?? Number.MAX_VALUE) + (firstRehearsalDate?.getTime() ?? Number.MAX_VALUE) ? firstGigDate : firstRehearsalDate; diff --git a/src/components/corps/position-infobox.tsx b/src/components/corps/position-infobox.tsx new file mode 100644 index 00000000..55571601 --- /dev/null +++ b/src/components/corps/position-infobox.tsx @@ -0,0 +1,161 @@ +import { IconMail } from '@tabler/icons-react'; +import ActionIcon from 'components/input/action-icon'; +import { filterNone } from 'utils/array'; +import { numberAndFullName } from 'utils/corps'; +import { lang } from 'utils/language'; + +interface Instrument { + instrument: { name: string }; + isMainInstrument: boolean; +} + +interface Role { + name: string; +} + +interface Corps { + id: string; + firstName: string; + lastName: string; + nickName: string | null; + pronouns: string | null; + number: number | null; + bNumber: number | null; + contactURL: string | null; + points: number; + firstGigDate: Date | undefined; + firstRehearsalDate: Date | undefined; + instruments: Instrument[]; + roles: Role[]; +} + +interface CorpsInfoboxProps { + corps: Corps; +} + +const genOtherInstrumentsString = (instruments: string[]) => { + const instrumentsLower = instruments.map((i) => i.toLowerCase()); + if (instrumentsLower.length === 0) return ''; + if (instrumentsLower.length === 1) return instrumentsLower[0] ?? ''; + return `${instrumentsLower + .slice(0, instrumentsLower.length - 1) + .join(', ')} och ${instrumentsLower[instrumentsLower.length - 1] ?? ''}`; +}; + +const roleToEmail: Record = { + Ordförande: "ordforande@bleckhornen.org", + ViceOrdförande: "vice@bleckhornen.org", + Sekreterare: "sekreterare@bleckhornen.org", + Kassör: "kassor@bleckhornen.org", +}; + +const roleListToEmail = (roles: Role[]) => { + const matchingRole = roles.find(r => roleToEmail[r.name]); + + return matchingRole ? ('mailto:' + roleToEmail[matchingRole.name]) : null; +} + +// A list of "instruments" which should have the prefix "är" +const beingPrefixes = ['dirigent', 'balett', 'slagverksfröken']; + + +const PositionInfobox = ({ corps }: CorpsInfoboxProps) => { + const corpsNameTemp = numberAndFullName(corps); + const corpsName = + corpsNameTemp.length > 25 + ? corpsNameTemp.slice(0, 25) + corpsNameTemp.slice(25).replace(' ', '\n') + : corpsNameTemp; + + const { + nickName, + pronouns, + contactURL, + points, + firstGigDate, + firstRehearsalDate, + instruments + } = corps; + + const mainInstrument = + instruments.find((i) => i.isMainInstrument)?.instrument.name ?? ''; + const otherInstruments = instruments + .filter((i) => !i.isMainInstrument) + .map((i) => i.instrument.name); + + const isPlayingMainInstrument = !beingPrefixes.includes( + mainInstrument.toLowerCase(), + ); + const isPlayingOtherInstrument = !otherInstruments.some((i) => + beingPrefixes.includes(i.toLowerCase()), + ); + + + const joinedAt = + (firstGigDate?.getTime() ?? Number.MAX_VALUE) < + (firstRehearsalDate?.getTime() ?? Number.MAX_VALUE) + ? firstGigDate + : firstRehearsalDate; + + const joinedMsg = `Gick med i corpset den ${joinedAt?.getDate()} ${joinedAt?.toLocaleDateString( + 'sv', + { month: 'long' }, + )} ${joinedAt?.getFullYear()}.`; + + const temp1 = isPlayingMainInstrument ? 'Spelar ' : 'Är '; + + // If the main instrument is the same as the other instruments, we don't need to specify it twice + const temp2 = + isPlayingMainInstrument !== isPlayingOtherInstrument + ? isPlayingOtherInstrument + ? 'spelar ' + : 'är ' + : ''; + + const instrumentsMsg = + temp1 + + (otherInstruments.length > 0 ? 'främst ' : '') + + mainInstrument.toLowerCase() + + (otherInstruments.length > 0 + ? ', men ' + temp2 + 'även ' + genOtherInstrumentsString(otherInstruments) + : '') + + '.'; + + const roleEmail = roleListToEmail(corps.roles) + const contact = roleEmail ? roleEmail : contactURL + + return ( +
    +
    +
    + {corpsName} + + {(contact) && ( + + + + )} +
    + {(nickName || pronouns) && ( +
    + {filterNone([corps.nickName, corps.pronouns]).join(' • ')} +
    + )} +
    +
    + {lang('Spelpoäng: ', 'Gig points: ')} + {points} +
    +
    +
    + {joinedAt && joinedMsg} {instrumentsMsg}{' '} + +
    + +
    + ); +}; + +export default PositionInfobox; diff --git a/src/components/navbar/index.tsx b/src/components/navbar/index.tsx index 88361c30..adb89379 100644 --- a/src/components/navbar/index.tsx +++ b/src/components/navbar/index.tsx @@ -3,6 +3,7 @@ import { IconClipboardList, IconHome, IconInfoSquare, + IconInfoHexagon, IconKey, IconLink, IconMicrophone2, @@ -61,6 +62,7 @@ const userTab: NavbarLinkGroup = { }, { label: lang('Sånger', 'Songs'), href: '/songs', icon: }, { label: lang('Länkar', 'Links'), href: '/links', icon: }, + { label: lang('Om Bleckhornen', 'About Bleckhornen'), href: '/info', icon: }, ], }; const adminTab: NavbarLinkGroup = { diff --git a/src/server/trpc/router/corps.ts b/src/server/trpc/router/corps.ts index 1e641bd8..8de11b88 100644 --- a/src/server/trpc/router/corps.ts +++ b/src/server/trpc/router/corps.ts @@ -141,6 +141,7 @@ export const corpsRouter = router({ nickName: z.string().transform(emptyToNull), pronouns: z.string().transform(emptyToNull), email: z.string(), + contactURL: z.string(), vegetarian: z.boolean(), vegan: z.boolean(), glutenFree: z.boolean(), @@ -154,6 +155,7 @@ export const corpsRouter = router({ nickName, pronouns, email, + contactURL, vegetarian, vegan, glutenFree, @@ -217,6 +219,7 @@ export const corpsRouter = router({ data: { nickName, pronouns, + contactURL, user: { update: { email: email.trim(), @@ -243,6 +246,7 @@ export const corpsRouter = router({ number: z.number().nullable(), bNumber: z.number().nullable(), email: z.string(), + contactURL: z.string(), mainInstrument: z.string(), otherInstruments: z.array(z.string()), roles: z.array(z.string()), @@ -261,6 +265,8 @@ export const corpsRouter = router({ input.nickName.trim().length > 0 ? input.nickName.trim() : null, pronouns: input.pronouns.trim().length > 0 ? input.pronouns.trim() : null, + contactURL: + input.contactURL.trim().length > 0 ? input.contactURL.trim() : null, number: input.number, bNumber: input.bNumber, instruments: {