Merge branch 'main' into LOA-Upgrades

This commit is contained in:
2025-12-11 19:49:55 -06:00
15 changed files with 320 additions and 131 deletions

View File

@@ -1,8 +1,8 @@
name: Continuous Deployment name: Continuous Deployment
on: on:
push: push:
branches: tags:
- main - '*'
jobs: jobs:
Deploy: Deploy:
@@ -17,15 +17,22 @@ jobs:
groupadd -g 989 nginx || true groupadd -g 989 nginx || true
useradd nginx -u 990 -g nginx -m || true useradd nginx -u 990 -g nginx -m || true
- name: Verify Node Environment - name: Update Node Environment
uses: actions/setup-node@v6
with:
node-version: 20.19
- name: Verify Local Environment
run: | run: |
which npm which npm
npm -v npm -v
which node which node
node -v node -v
which sed
sed --version
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
ref: 'main' ref: 'main'
@@ -36,38 +43,53 @@ jobs:
cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config
chown nginx:nginx .git/config chown nginx:nginx .git/config
- name: Fix File Permissions
run: |
sudo chown -R nginx:nginx /var/www/html/milsim-site-v4
sudo chmod -R u+w /var/www/html/milsim-site-v4
- name: Update Application Code - name: Update Application Code
run: | run: |
cd /var/www/html/milsim-site-v4 cd /var/www/html/milsim-site-v4
version=`git log -1 --format=%H`
echo "Current Revision: $version"
echo "Updating to: ${{ github.sha }}
sudo -u nginx git reset --hard sudo -u nginx git reset --hard
sudo -u nginx git pull origin main sudo -u nginx git pull origin main
sudo -u nginx git pull origin main
new_version=`git log -1 --format=%H`
echo "Sucessfully updated to: $new_version
- name: Update Shared Dependencies - name: Update Shared Dependencies and Fix Permissions
run: | run: |
cd /var/www/html/milsim-site-v4/shared cd /var/www/html/milsim-site-v4/shared
sudo -u nginx -E npm install npm install
chown -R nginx:nginx .
- name: Update UI Dependencies - name: Update UI Dependencies and Fix Permissions
run: | run: |
cd /var/www/html/milsim-site-v4/ui cd /var/www/html/milsim-site-v4/ui
sudo -u nginx -E npm install npm install
chown -R nginx:nginx .
- name: Update API Dependencies - name: Update API Dependencies and Fix Permissions
run: | run: |
cd /var/www/html/milsim-site-v4/api cd /var/www/html/milsim-site-v4/api
sudo -u nginx -E npm install npm install
chown -R nginx:nginx .
- name: Build UI - name: Build UI / Update Version / Fix Permissions
run: | run: |
cd /var/www/html/milsim-site-v4/ui cd /var/www/html/milsim-site-v4/ui
sudo -u nginx -E npm run build npm run build
version=`git describe --abbrev=0 --tags`
sed -i "s/VITE_APPLICATION_VERSION=.*/VITE_APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Build API - name: Build API / Update Version / Fix Permissions
run: | run: |
cd /var/www/html/milsim-site-v4/api cd /var/www/html/milsim-site-v4/api
sudo -u nginx -E npm run build npm run build
version=`git describe --abbrev=0 --tags`
sed -i "s/APPLICATION_VERSION=.*/APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Reset File Permissions
run: |
sudo chown -R nginx:nginx /var/www/html/milsim-site-v4
sudo chmod -R u+w /var/www/html/milsim-site-v4

View File

@@ -0,0 +1,89 @@
name: Continuous Integration
on:
push:
branches:
- main
jobs:
Deploy:
name: Update Development
runs-on: ubuntu-latest
container:
volumes:
- /var/www/html/milsim-site-v4-dev:/var/www/html/milsim-site-v4:z
steps:
- name: Setup Local Environment
run: |
groupadd -g 989 nginx || true
useradd nginx -u 990 -g nginx -m || true
- name: Update Node Environment
uses: actions/setup-node@v6
with:
node-version: 20.19
- name: Verify Local Environment
run: |
which npm
npm -v
which node
node -v
which sed
sed --version
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: 'main'
- name: Token Copy
run: |
cd /var/www/html/milsim-site-v4
cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config
chown nginx:nginx .git/config
- name: Update Application Code
run: |
cd /var/www/html/milsim-site-v4
sudo -u nginx git reset --hard
sudo -u nginx git pull origin main
- name: Update Shared Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/shared
npm install
chown -R nginx:nginx .
- name: Update UI Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/ui
npm install
chown -R nginx:nginx .
- name: Update API Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/api
npm install
chown -R nginx:nginx .
- name: Build UI / Update Version / Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/ui
npm run build
version=`git rev-parse --short=10 HEAD`
sed -i "s/VITE_APPLICATION_VERSION=.*/VITE_APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Build API / Update Version / Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/api
npm run build
version=`git rev-parse --short=10 HEAD`
sed -i "s/APPLICATION_VERSION=.*/APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Reset File Permissions
run: |
sudo chown -R nginx:nginx /var/www/html/milsim-site-v4
sudo chmod -R u+w /var/www/html/milsim-site-v4

View File

@@ -19,6 +19,8 @@ AUTH_END_SESSION_URI=
SERVER_PORT=3000 SERVER_PORT=3000
CLIENT_URL= # This is whatever URL the client web app is served on CLIENT_URL= # This is whatever URL the client web app is served on
CLIENT_DOMAIN= #whatever.com CLIENT_DOMAIN= #whatever.com
APPLICATION_VERSION= # Should match release tag
APPLICATION_ENVIRONMENT= # dev / prod
# Glitchtip # Glitchtip
GLITCHTIP_DSN= GLITCHTIP_DSN=

View File

@@ -20,11 +20,14 @@ const port = process.env.SERVER_PORT;
//glitchtip setup //glitchtip setup
const sentry = require('@sentry/node'); const sentry = require('@sentry/node');
if (process.env.DISABLE_GLITCHTIP) { if (process.env.DISABLE_GLITCHTIP === "true") {
console.log("Glitchtip disabled") console.log("Glitchtip disabled")
} else { } else {
let dsn = process.env.GLITCHTIP_DSN; let dsn = process.env.GLITCHTIP_DSN;
sentry.init({ dsn: dsn }); let release = process.env.APPLICATION_VERSION;
let environment = process.env.APPLICATION_ENVIRONMENT;
console.log(release, environment)
sentry.init({ dsn: dsn, release: release, environment: environment });
console.log("Glitchtip initialized"); console.log("Glitchtip initialized");
} }
@@ -58,6 +61,7 @@ const { roles, memberRoles } = require('./routes/roles');
const { courseRouter, eventRouter } = require('./routes/course'); const { courseRouter, eventRouter } = require('./routes/course');
const { calendarRouter } = require('./routes/calendar') const { calendarRouter } = require('./routes/calendar')
const morgan = require('morgan'); const morgan = require('morgan');
const { env } = require('process');
app.use('/application', applicationsRouter); app.use('/application', applicationsRouter);
app.use('/ranks', ranks); app.use('/ranks', ranks);

View File

@@ -21,12 +21,13 @@ passport.use(new OpenIDConnectStrategy({
scope: ['openid', 'profile'] scope: ['openid', 'profile']
}, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { }, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) {
// console.log('--- OIDC verify() called ---'); console.log('--- OIDC verify() called ---');
// console.log('issuer:', issuer); console.log('issuer:', issuer);
// console.log('sub:', sub); console.log('sub:', sub);
// console.log('profile:', JSON.stringify(profile, null, 2)); // console.log('profile:', JSON.stringify(profile, null, 2));
// console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); console.log('profile:', profile);
// console.log('preferred_username:', jwtClaims?.preferred_username); console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2));
console.log('preferred_username:', jwtClaims?.preferred_username);
const con = await pool.getConnection(); const con = await pool.getConnection();
try { try {

View File

@@ -123,15 +123,9 @@ export async function setAttendanceStatus(memberID: number, eventID: number, sta
} }
export async function getEventAttendance(eventID: number): Promise<CalendarSignup[]> { export async function getEventAttendance(eventID: number): Promise<CalendarSignup[]> {
const sql = `
SELECT
s.member_id,
s.status,
m.name AS member_name
FROM calendar_events_signups s
LEFT JOIN members m ON s.member_id = m.id
WHERE s.event_id = ?
`;
return await pool.query(sql, [eventID]); const sql = "CALL `sp_GetCalendarEventSignups`(?)"
const res = await pool.query(sql, [eventID]);
console.log(res[0]);
return res[0];
} }

View File

@@ -5,6 +5,7 @@ module.exports = {
script: 'built/api/src/index.js', script: 'built/api/src/index.js',
watch: ['.env', 'built'], watch: ['.env', 'built'],
ignore_watch: ['.gitignore', '\.json', 'src', '\.db', 'node_modules'], ignore_watch: ['.gitignore', '\.json', 'src', '\.db', 'node_modules'],
appendEnvToName: true,
watch_options: { watch_options: {
usePolling: true, usePolling: true,
interval: 10000 interval: 10000

View File

@@ -26,6 +26,7 @@ export interface CalendarSignup {
eventID: number; eventID: number;
status: CalendarAttendance; status: CalendarAttendance;
member_name?: string; member_name?: string;
member_unit?: string;
} }
export interface CalendarEventShort { export interface CalendarEventShort {

View File

@@ -2,6 +2,8 @@
VITE_APIHOST= VITE_APIHOST=
VITE_DOCHOST= # https://bookstack.whatever.com/api VITE_DOCHOST= # https://bookstack.whatever.com/api
VITE_ENVIRONMENT= # dev / prod VITE_ENVIRONMENT= # dev / prod
VITE_APPLICATION_VERSION= # Should match release tag
# Glitchtip # Glitchtip
VITE_GLITCHTIP_DSN= VITE_GLITCHTIP_DSN=

View File

@@ -156,6 +156,12 @@ function blurAfter() {
<RouterLink to="/join" @click="blurAfter">Join</RouterLink> <RouterLink to="/join" @click="blurAfter">Join</RouterLink>
</NavigationMenuLink> </NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
<!-- Calendar -->
<NavigationMenuItem>
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
<RouterLink to="/calendar" @click="blurAfter">Calendar</RouterLink>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CalendarEvent, CalendarSignup } from '@shared/types/calendar' import type { CalendarEvent, CalendarSignup } from '@shared/types/calendar'
import { CircleAlert, Clock, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next'; import { CircleAlert, Clock4, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import ButtonGroup from '../ui/button-group/ButtonGroup.vue'; import ButtonGroup from '../ui/button-group/ButtonGroup.vue';
import Button from '../ui/button/Button.vue'; import Button from '../ui/button/Button.vue';
import { CalendarAttendance, getCalendarEvent, setCalendarEventAttendance, setCancelCalendarEvent } from '@/api/calendar'; import { CalendarAttendance, getCalendarEvent, setCalendarEventAttendance, setCancelCalendarEvent } from '@/api/calendar';
@@ -11,24 +11,13 @@ import DropdownMenu from '../ui/dropdown-menu/DropdownMenu.vue';
import DropdownMenuTrigger from '../ui/dropdown-menu/DropdownMenuTrigger.vue'; import DropdownMenuTrigger from '../ui/dropdown-menu/DropdownMenuTrigger.vue';
import DropdownMenuContent from '../ui/dropdown-menu/DropdownMenuContent.vue'; import DropdownMenuContent from '../ui/dropdown-menu/DropdownMenuContent.vue';
import DropdownMenuItem from '../ui/dropdown-menu/DropdownMenuItem.vue'; import DropdownMenuItem from '../ui/dropdown-menu/DropdownMenuItem.vue';
import { Calendar } from 'lucide-vue-next';
const route = useRoute(); const route = useRoute();
// const eventID = computed(() => {
// const id = route.params.id;
// if (typeof id === 'string') return id;
// return undefined;
// });
const loaded = ref<boolean>(false); const loaded = ref<boolean>(false);
const activeEvent = ref<CalendarEvent | null>(null); const activeEvent = ref<CalendarEvent | null>(null);
// onMounted(async () => {
// let eventID = route.params.id;
// console.log(eventID);
// activeEvent.value = await getCalendarEvent(Number(eventID));
// loaded.value = true;
// });
watch( watch(
() => route.params.id, () => route.params.id,
async (id) => { async (id) => {
@@ -45,23 +34,27 @@ const emit = defineEmits<{
(e: 'edit', event: CalendarEvent): void (e: 'edit', event: CalendarEvent): void
}>() }>()
// const activeEvent = computed(() => props.event) const dateFmt = new Intl.DateTimeFormat(undefined, {
weekday: 'long', month: 'short', day: 'numeric',
const startFmt = new Intl.DateTimeFormat(undefined, {
weekday: 'short', year: 'numeric', month: 'short', day: 'numeric',
hour: 'numeric', minute: '2-digit'
}) })
const endFmt = new Intl.DateTimeFormat(undefined, {
const timeFmt = new Intl.DateTimeFormat(undefined, {
hour: 'numeric', minute: '2-digit' hour: 'numeric', minute: '2-digit'
}) })
const whenText = computed(() => { const dateText = computed(() => {
if (!activeEvent.value?.start) return '' let start = dateFmt.format(new Date(activeEvent.value.start));
const s = new Date(activeEvent.value.start) let end = dateFmt.format(new Date(activeEvent.value.end));
const e = activeEvent.value?.end ? new Date(activeEvent.value.end) : null if (start === end)
return e return start;
? `${startFmt.format(s)} ${endFmt.format(e)}` else
: `${startFmt.format(s)}` return `${start} - ${end}`;
})
const timeText = computed(() => {
let startTime = timeFmt.format(new Date(activeEvent.value.start))
let endTime = timeFmt.format(new Date(activeEvent.value.end))
return [startTime, endTime]
}) })
const attending = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Attending) }) const attending = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Attending) })
@@ -71,6 +64,7 @@ const viewedState = ref<CalendarAttendance>(CalendarAttendance.Attending);
let user = useUserStore(); let user = useUserStore();
const myAttendance = computed<CalendarSignup | null>(() => { const myAttendance = computed<CalendarSignup | null>(() => {
if (!user.isLoggedIn) return null;
return activeEvent.value.eventSignups.find( return activeEvent.value.eventSignups.find(
(s) => s.member_id === user.user.id (s) => s.member_id === user.user.id
) || null; ) || null;
@@ -83,6 +77,7 @@ async function setAttendance(state: CalendarAttendance) {
} }
const canEditEvent = computed(() => { const canEditEvent = computed(() => {
if (!user.isLoggedIn) return false;
if (user.user.id == activeEvent.value.creator_id) if (user.user.id == activeEvent.value.creator_id)
return true; return true;
}); });
@@ -97,17 +92,94 @@ async function forceReload() {
activeEvent.value = await getCalendarEvent(activeEvent.value.id); activeEvent.value = await getCalendarEvent(activeEvent.value.id);
} }
defineExpose({forceReload}) const isPast = computed(() => {
const end = new Date(activeEvent.value.end)
// is current date later than end date
return new Date() < end;
})
const attendanceTab = ref<"Alpha" | "Echo" | "Other">("Alpha");
const attendanceList = computed<CalendarSignup[]>(() => {
let out: CalendarSignup[] = [];
if (attendanceTab.value === 'Alpha') {
out = activeEvent.value.eventSignups?.filter((s) => s.member_unit === 'Alpha Company');
} else if (attendanceTab.value === 'Echo') {
out = activeEvent.value.eventSignups?.filter((s) => s.member_unit === 'Echo Company')
} else {
out = activeEvent.value.eventSignups?.filter((s) => s.member_unit != 'Alpha Company' && s.member_unit != 'Echo Company')
}
const statusOrder: Record<CalendarAttendance, number> = {
[CalendarAttendance.Attending]: 1,
[CalendarAttendance.Maybe]: 2,
[CalendarAttendance.NotAttending]: 3,
};
out.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
return out;
})
const attendanceCountsByGroup = computed(() => {
const signups = activeEvent.value.eventSignups ?? [];
return {
Alpha: signups.filter(s => s.member_unit === "Alpha Company").length,
Echo: signups.filter(s => s.member_unit === "Echo Company").length,
Other: signups.filter(s =>
s.member_unit !== "Alpha Company" &&
s.member_unit !== "Echo Company"
).length,
};
});
const attendanceStatusSummary = computed(() => {
const signups = activeEvent.value.eventSignups ?? [];
return {
attending: signups.filter(s => s.status === CalendarAttendance.Attending).length,
maybe: signups.filter(s => s.status === CalendarAttendance.Maybe).length,
notAttending: signups.filter(s => s.status === CalendarAttendance.NotAttending).length,
};
});
const statusColor = (status: CalendarAttendance) => {
switch (status) {
case CalendarAttendance.Attending:
return "text-success";
case CalendarAttendance.Maybe:
return "text-yellow-600";
case CalendarAttendance.NotAttending:
return "text-destructive";
default:
return "";
}
};
const displayStatus = (status: CalendarAttendance) => {
switch (status) {
case CalendarAttendance.Attending:
return "Attending";
case CalendarAttendance.Maybe:
return "Maybe";
case CalendarAttendance.NotAttending:
return "Declined";
default:
return status;
}
};
defineExpose({ forceReload })
</script> </script>
<template> <template>
<div v-if="loaded"> <div v-if="loaded">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between gap-3 border-b px-4 py-3"> <div class="flex items-center justify-between gap-3 border-b px-4 py-3 h-14">
<h2 class="text-lg font-semibold break-all"> <h2 class="text-lg font-semibold break-all">
{{ activeEvent?.name || 'Event' }} {{ activeEvent?.name || 'Event' }}
</h2> </h2>
<div class="flex gap-4"> <div class="flex gap-4 items-center">
<DropdownMenu v-if="canEditEvent"> <DropdownMenu v-if="canEditEvent">
<DropdownMenuTrigger> <DropdownMenuTrigger>
<button <button
@@ -119,8 +191,7 @@ defineExpose({forceReload})
<DropdownMenuItem @click="emit('edit', activeEvent)"> <DropdownMenuItem @click="emit('edit', activeEvent)">
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem v-if="activeEvent.cancelled" <DropdownMenuItem v-if="activeEvent.cancelled" @click="setCancel(false)">
@click="setCancel(false)">
Un-Cancel Un-Cancel
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem v-else @click="setCancel(true)"> <DropdownMenuItem v-else @click="setCancel(true)">
@@ -142,7 +213,7 @@ defineExpose({forceReload})
<CircleAlert></CircleAlert> This event has been cancelled <CircleAlert></CircleAlert> This event has been cancelled
</div> </div>
</section> </section>
<section class="w-full"> <section v-if="isPast && user.isLoggedIn" class="w-full">
<ButtonGroup class="flex w-full"> <ButtonGroup class="flex w-full">
<Button variant="outline" <Button variant="outline"
:class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''" :class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''"
@@ -155,24 +226,21 @@ defineExpose({forceReload})
@click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button> @click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button>
</ButtonGroup> </ButtonGroup>
</section> </section>
<!-- When --> <!-- Meta -->
<section v-if="whenText" class="space-y-2 w-full"> <section class="space-y-3 w-full">
<div class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm"> <p class="text-lg font-semibold">Details</p>
<Clock class="size-4 opacity-80" /> <div class="text-foreground/80 flex gap-3 items-center">
<span class="font-medium">{{ whenText }}</span> <Calendar :size="20"></Calendar> {{ dateText }}
</div>
<div class="text-foreground/80 flex gap-3 items-center">
<Clock4 :size="20"></Clock4> {{ timeText[0] }} - {{ timeText[1] }}
</div>
<div class="text-foreground/80 flex gap-3 items-center">
<MapPin :size="20"></MapPin> {{ activeEvent.location || "Unknown" }}
</div>
<div class="text-foreground/80 flex gap-3 items-center">
<User :size="20"></User> {{ activeEvent.creator_name || "Unknown User" }}
</div> </div>
</section>
<!-- Quick meta chips -->
<section class="flex flex-wrap gap-2 w-full">
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
<MapPin class="size-3.5 opacity-80" />
<span class="font-medium">{{ activeEvent.location || "Unknown" }}</span>
</span>
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
<User class="size-3.5 opacity-80" />
<span class="font-medium">Created by: {{ activeEvent.creator_name || "Unknown User"
}}</span>
</span>
</section> </section>
<!-- Description --> <!-- Description -->
<section class="space-y-2 w-full"> <section class="space-y-2 w-full">
@@ -181,46 +249,41 @@ defineExpose({forceReload})
{{ activeEvent.description }} {{ activeEvent.description }}
</p> </p>
</section> </section>
<!-- Attendance --> <!-- attendance -->
<section class="space-y-2 w-full"> <section class="space-y-2 w-full">
<p class="text-lg font-semibold">Attendance</p> <div class="flex items-center gap-5">
<p class="text-lg font-semibold">Attendance</p>
<!-- <div class="text-muted-foreground flex gap-6">
<p>Going <span class="ml-1">{{ attendanceStatusSummary.attending }}</span></p>
<p>Maybe <span class="ml-1">{{ attendanceStatusSummary.maybe }}</span></p>
<p>Declined <span class="ml-1">{{ attendanceStatusSummary.notAttending }}</span></p>
</div> -->
</div>
<div class="flex flex-col border bg-muted/50 rounded-lg min-h-24 my-2"> <div class="flex flex-col border bg-muted/50 rounded-lg min-h-24 my-2">
<div class="flex w-full pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer"> <div class="flex w-full pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer">
<label <label :class="attendanceTab === 'Alpha' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
:class="viewedState === CalendarAttendance.Attending ? 'border-b-3 border-foreground' : 'mb-[2px]'" @click="attendanceTab = 'Alpha'">Alpha {{ attendanceCountsByGroup.Alpha }}</label>
@click="viewedState = CalendarAttendance.Attending">Going {{ attending.length }}</label> <label :class="attendanceTab === 'Echo' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
<label @click="attendanceTab = 'Echo'">Echo {{ attendanceCountsByGroup.Echo }}</label>
:class="viewedState === CalendarAttendance.Maybe ? 'border-b-3 border-foreground' : 'mb-[2px]'" <label :class="attendanceTab === 'Other' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="viewedState = CalendarAttendance.Maybe">Maybe {{ maybe.length }}</label> @click="attendanceTab = 'Other'">Other {{ attendanceCountsByGroup.Other }}</label>
<label
:class="viewedState === CalendarAttendance.NotAttending ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="viewedState = CalendarAttendance.NotAttending">Declined {{ declined.length
}}</label>
</div> </div>
<div class="px-5 py-4 min-h-28"> <div class="pb-1 min-h-48">
<div v-if="viewedState === CalendarAttendance.Attending" v-for="person in attending"> <div class="grid grid-cols-2 font-semibold text-muted-foreground border-b py-1 px-3 mb-2">
<p>{{ person.member_name }}</p> <p>Name</p>
<p class="text-right">Status</p>
</div> </div>
<div v-if="viewedState === CalendarAttendance.Maybe" v-for="person in maybe">
<p>{{ person.member_name }}</p> <div v-for="person in attendanceList" :key="person.member_id"
</div> class="grid grid-cols-2 py-1 *:px-3 hover:bg-muted">
<div v-if="viewedState === CalendarAttendance.NotAttending" v-for="person in declined">
<p>{{ person.member_name }}</p> <p>{{ person.member_name }}</p>
<p :class="statusColor(person.status)" class="text-right">
{{ displayStatus(person.status) }}
</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<!-- Footer (optional actions) -->
<!-- <div class="border-t px-4 py-3 flex items-center justify-end gap-2">
<button class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted/40 transition">
Edit
</button>
<button
class="rounded-md bg-primary text-primary-foreground px-3 py-1.5 text-sm hover:opacity-90 transition">
Open details
</button>
</div> -->
</div> </div>
</template> </template>

View File

@@ -20,10 +20,12 @@ const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
if (!import.meta.env.VITE_DISABLE_GLITCHTIP) { if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") {
console.log("Glitchtip disabled");
} else {
let dsn = import.meta.env.VITE_GLITCHTIP_DSN; let dsn = import.meta.env.VITE_GLITCHTIP_DSN;
let environment = import.meta.env.VITE_ENVIRONMENT; let environment = import.meta.env.VITE_ENVIRONMENT;
let release = import.meta.env.VITE_APPLICATION_VERSION;
Sentry.init({ Sentry.init({
app, app,
dsn: dsn, dsn: dsn,
@@ -32,7 +34,7 @@ if (!import.meta.env.VITE_DISABLE_GLITCHTIP) {
], ],
tracesSampleRate: 0.01, tracesSampleRate: 0.01,
environment: environment, environment: environment,
release: 'release tag' release: release
}); });
} }

View File

@@ -3,15 +3,14 @@ import { ref, watch, nextTick, computed, onMounted } from 'vue'
import FullCalendar from '@fullcalendar/vue3' import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid' import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction' import interactionPlugin from '@fullcalendar/interaction'
import { X, Clock, MapPin, User, ListTodo, ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next' import { ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next'
import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue' import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue'
import { getCalendarEvent, getMonthCalendarEvents } from '@/api/calendar' import { CalendarEvent } from '@shared/types/calendar'
import { CalendarEvent, CalendarEventShort } from '@shared/types/calendar'
import { Calendar } from '@fullcalendar/core'
import ViewCalendarEvent from '@/components/calendar/ViewCalendarEvent.vue' import ViewCalendarEvent from '@/components/calendar/ViewCalendarEvent.vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useCalendarEvents } from '@/composables/useCalendarEvents' import { useCalendarEvents } from '@/composables/useCalendarEvents'
import { useCalendarNavigation } from '@/composables/useCalendarNavigation' import { useCalendarNavigation } from '@/composables/useCalendarNavigation'
import { useUserStore } from '@/stores/user'
const monthLabels = [ const monthLabels = [
'January', 'February', 'March', 'April', 'May', 'June', 'January', 'February', 'March', 'April', 'May', 'June',
@@ -24,13 +23,14 @@ function api() {
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const userStore = useUserStore();
function buildFullDate(month: number, year: number): Date { function buildFullDate(month: number, year: number): Date {
return new Date(year, month, 1); //default to first of month return new Date(year, month, 1); //default to first of month
} }
const { selectedMonth, selectedYear, years, goPrev, goNext, goToday, onDatesSet, goToSelectedDate } = useCalendarNavigation(api) const { selectedMonth, selectedYear, years, goPrev, goNext, goToday, onDatesSet, goToSelectedDate } = useCalendarNavigation(api)
const { events, loadEvents} = useCalendarEvents(selectedMonth, selectedYear); const { events, loadEvents } = useCalendarEvents(selectedMonth, selectedYear);
const panelOpen = ref(false) const panelOpen = ref(false)
const activeEvent = ref<CalendarEvent | null>(null) const activeEvent = ref<CalendarEvent | null>(null)
@@ -48,6 +48,7 @@ const dialogRef = ref<any>(null)
// NEW: handle day/time slot clicks to start creating an event // NEW: handle day/time slot clicks to start creating an event
function onDateClick(arg: { dateStr: string }) { function onDateClick(arg: { dateStr: string }) {
if (!userStore.isLoggedIn) return;
dialogRef.value?.openDialog(arg.dateStr); dialogRef.value?.openDialog(arg.dateStr);
// For now, just open the panel with a draft payload. // For now, just open the panel with a draft payload.
// activeEvent.value = { // activeEvent.value = {
@@ -202,7 +203,7 @@ onMounted(() => {
@click="goToday"> @click="goToday">
Today Today
</button> </button>
<button <button v-if="userStore.isLoggedIn"
class="cursor-pointer ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90" class="cursor-pointer ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90"
@click="onCreateEvent"> @click="onCreateEvent">
<Plus class="h-4 w-4" /> <Plus class="h-4 w-4" />
@@ -216,7 +217,8 @@ onMounted(() => {
<aside v-if="panelOpen" <aside v-if="panelOpen"
class="3xl:w-lg 2xl:w-md border-l bg-card text-foreground flex flex-col overflow-auto scrollbar-themed" class="3xl:w-lg 2xl:w-md border-l bg-card text-foreground flex flex-col overflow-auto scrollbar-themed"
:style="{ height: 'calc(100vh - 61px)', position: 'sticky', top: '64px' }"> :style="{ height: 'calc(100vh - 61px)', position: 'sticky', top: '64px' }">
<ViewCalendarEvent ref="eventViewRef" @close="() => { router.push('/calendar'); }" @reload="loadEvents()" @edit="(val) => {dialogRef.openDialog(null, 'edit', val)}"> <ViewCalendarEvent ref="eventViewRef" @close="() => { router.push('/calendar'); }"
@reload="loadEvents()" @edit="(val) => { dialogRef.openDialog(null, 'edit', val) }">
</ViewCalendarEvent> </ViewCalendarEvent>
</aside> </aside>
</div> </div>

View File

@@ -171,9 +171,9 @@ const finalPanel = ref<'app' | 'message'>('message');
<li>When prompted, choose <em>“Yes”</em> to download all associated mods.</li> <li>When prompted, choose <em>“Yes”</em> to download all associated mods.</li>
</ul> </ul>
<p> <p>
<a href="https://www.guilded.gg/Iceberg-gaming/groups/v3j2vAP3/channels/6979335e-60f7-4ab9-9590-66df69367d1e/docs/2013948655" <a href="https://docs.iceberg-gaming.com/books/member-guides/page/new-member-setup-onboarding"
class="text-primary underline" target="_blank"> class="text-primary underline" target="_blank">
Click here for the full installation guide Click here for the full installation guide (Requires Sign-in)
</a> </a>
</p> </p>
<!-- CONTACT SECTION --> <!-- CONTACT SECTION -->
@@ -211,7 +211,7 @@ const finalPanel = ref<'app' | 'message'>('message');
our forums and introduce yourself. our forums and introduce yourself.
</p> </p>
<p> <p>
If you have any questions, feel free to reach out on TeamSpeak, Discord, or Guilded, If you have any questions, feel free to reach out on TeamSpeak or Discord
someone someone
will always be around to help. will always be around to help.
</p> </p>

View File

@@ -16,8 +16,8 @@ const router = createRouter({
{ path: '/loa', component: () => import('@/pages/SubmitLOA.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/loa', component: () => import('@/pages/SubmitLOA.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/transfer', component: () => import('@/pages/Transfer.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/transfer', component: () => import('@/pages/Transfer.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true }, }, { path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { }, },
{ path: '/calendar/event/:id', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true }, }, { path: '/calendar/event/:id', component: () => import('@/pages/Calendar.vue'), meta: { }, },
{ path: '/trainingReport', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },