Added first draft of story
This commit is contained in:
@@ -20,8 +20,7 @@ const { setActiveCalendar } = useCalendar()
|
|||||||
setActiveCalendar(props.calendarData, props.categories)
|
setActiveCalendar(props.calendarData, props.categories)
|
||||||
|
|
||||||
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
|
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
|
||||||
|
const { isReadOnly } = storeToRefs(useCalendar())
|
||||||
// const { setCharacters } = useCharacters()
|
|
||||||
|
|
||||||
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
|
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
|
||||||
switch (currentConfig.viewType) {
|
switch (currentConfig.viewType) {
|
||||||
@@ -47,6 +46,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full relative">
|
<div class="h-full w-full relative">
|
||||||
|
{{ isReadOnly }}
|
||||||
<div class="h-full grid grid-rows-[auto,1fr]">
|
<div class="h-full grid grid-rows-[auto,1fr]">
|
||||||
<CalendarMenu />
|
<CalendarMenu />
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { z } from "zod"
|
|||||||
import type { CalendarEvent } from "./CalendarEvent"
|
import type { CalendarEvent } from "./CalendarEvent"
|
||||||
import { calendarMonthSchema, type CalendarMonth } from "./CalendarMonth"
|
import { calendarMonthSchema, type CalendarMonth } from "./CalendarMonth"
|
||||||
import { dateSchema, type RPGDate } from "./Date"
|
import { dateSchema, type RPGDate } from "./Date"
|
||||||
|
import type { World } from "./World"
|
||||||
|
|
||||||
|
export type CalendarState = "published" | "draft" | "archived"
|
||||||
|
|
||||||
export interface CalendarConfig {
|
export interface CalendarConfig {
|
||||||
months: CalendarMonth[]
|
months: CalendarMonth[]
|
||||||
@@ -10,9 +13,12 @@ export interface CalendarConfig {
|
|||||||
|
|
||||||
export interface Calendar extends CalendarConfig {
|
export interface Calendar extends CalendarConfig {
|
||||||
id?: number
|
id?: number
|
||||||
|
shortId: string
|
||||||
name: string
|
name: string
|
||||||
events: CalendarEvent[]
|
events: CalendarEvent[]
|
||||||
|
state: CalendarState
|
||||||
color?: string
|
color?: string
|
||||||
|
world?: World
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postCalendarSchema = z.object({
|
export const postCalendarSchema = z.object({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
"@nuxtjs/supabase",
|
"@nuxtjs/supabase",
|
||||||
@@ -37,7 +37,10 @@ export default defineNuxtConfig({
|
|||||||
supabase: {
|
supabase: {
|
||||||
redirectOptions: {
|
redirectOptions: {
|
||||||
login: "/",
|
login: "/",
|
||||||
callback: "/my/"
|
callback: "/my/",
|
||||||
|
exclude: [
|
||||||
|
"/calendars(/*)?"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
42
pages/calendars/[id].vue
Normal file
42
pages/calendars/[id].vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
|
import type { Category } from "~/models/Category";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const shortId = route.params.id
|
||||||
|
|
||||||
|
const { data: calendarData, pending: calPending } = await useLazyFetch("/api/calendars/query", { key: `calendar-${shortId}`, query: { shortId, full: true } })
|
||||||
|
const { data: catData, pending: catPending } = await useLazyFetch("/api/calendars/categories/query", { key: `categories-${shortId}` })
|
||||||
|
|
||||||
|
const cal = computed<Calendar>(() => calendarData?.value?.data as Calendar)
|
||||||
|
const categories = computed<Category[]>(() => catData?.value?.data as Category[])
|
||||||
|
|
||||||
|
const user = useSupabaseUser()
|
||||||
|
|
||||||
|
const { setReadStatus } = useCalendar()
|
||||||
|
setReadStatus(user.value, cal.value)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="calPending || catPending" class="h-full w-full grid place-items-center">
|
||||||
|
<Head>
|
||||||
|
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<div class="grid gap-2 justify-items-center opacity-50">
|
||||||
|
<p>
|
||||||
|
{{ $t('entity.calendar.isLoading') }}
|
||||||
|
</p>
|
||||||
|
<PhCircleNotch size="50" class="animate-spin"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="cal && categories" class="h-full w-full">
|
||||||
|
<Head>
|
||||||
|
<Title>{{ cal.name }}</Title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Calendar :calendar-data="cal" :categories />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -23,6 +23,9 @@ const { data: catData, pending: catPending } = await useLazyFetch("/api/calendar
|
|||||||
|
|
||||||
const cal = computed<Calendar>(() => calendarData?.value?.data as Calendar)
|
const cal = computed<Calendar>(() => calendarData?.value?.data as Calendar)
|
||||||
const categories = computed<Category[]>(() => catData?.value?.data as Category[])
|
const categories = computed<Category[]>(() => catData?.value?.data as Category[])
|
||||||
|
|
||||||
|
const { setReadStatus } = useCalendar()
|
||||||
|
setReadStatus(user.value, cal.value)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Calendar } from "~/models/CalendarConfig";
|
|||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
id: z.number({ coerce: true }).positive().int().optional(),
|
id: z.number({ coerce: true }).positive().int().optional(),
|
||||||
|
shortId: z.string().optional(),
|
||||||
full: z.boolean({ coerce: true }).optional()
|
full: z.boolean({ coerce: true }).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -15,16 +16,20 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const partialFields = `
|
const partialFields = `
|
||||||
id,
|
id,
|
||||||
|
shortId:short_id,
|
||||||
name,
|
name,
|
||||||
today,
|
today,
|
||||||
months:calendar_months (*)
|
months:calendar_months (*),
|
||||||
|
state
|
||||||
`
|
`
|
||||||
|
|
||||||
const fullFields = `
|
const fullFields = `
|
||||||
id,
|
id,
|
||||||
|
shortId:short_id,
|
||||||
name,
|
name,
|
||||||
today,
|
today,
|
||||||
months:calendar_months (*),
|
months:calendar_months (*),
|
||||||
|
state,
|
||||||
events:calendar_events (
|
events:calendar_events (
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
@@ -36,6 +41,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
wiki,
|
wiki,
|
||||||
category:calendar_event_categories!calendar_events_category_fkey (*),
|
category:calendar_event_categories!calendar_events_category_fkey (*),
|
||||||
secondaryCategories:calendar_event_categories!calendar_event_categories_links (*)
|
secondaryCategories:calendar_event_categories!calendar_event_categories_links (*)
|
||||||
|
),
|
||||||
|
world:worlds (
|
||||||
|
id,
|
||||||
|
gmId:gm_id
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -47,6 +56,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
output = client.from("calendars").select(partialFields)
|
output = client.from("calendars").select(partialFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.shortId) {
|
||||||
|
return output.eq("short_id", query.shortId).limit(1).single<Calendar>()
|
||||||
|
}
|
||||||
|
|
||||||
if (query.id) {
|
if (query.id) {
|
||||||
return output.eq("id", query.id).limit(1).single<Calendar>()
|
return output.eq("id", query.id).limit(1).single<Calendar>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
|
|
||||||
const activeCalendar = ref<{ id: number; name: string; today: RPGDate } | null>(null)
|
const activeCalendar = ref<{ id: number; name: string; today: RPGDate } | null>(null)
|
||||||
|
|
||||||
|
const isReadOnly = ref<boolean>(true)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function setReadStatus(user: any, calendar: Calendar) {
|
||||||
|
isReadOnly.value = (!user) || (calendar.world?.gmId !== user?.id)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Month list (queried from API)
|
* Month list (queried from API)
|
||||||
*/
|
*/
|
||||||
@@ -59,7 +65,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
activeCalendar.value = {
|
activeCalendar.value = {
|
||||||
id: calendarData.id,
|
id: calendarData.id,
|
||||||
name: calendarData.name,
|
name: calendarData.name,
|
||||||
today: calendarData.today
|
today: calendarData.today,
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultDate(activeCalendar.value.today)
|
setDefaultDate(activeCalendar.value.today)
|
||||||
@@ -901,6 +907,8 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isReadOnly,
|
||||||
|
setReadStatus,
|
||||||
setActiveCalendar,
|
setActiveCalendar,
|
||||||
activeCalendar,
|
activeCalendar,
|
||||||
months,
|
months,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ create type public.app_permission as enum ('events.see.hidden', 'users.ban');
|
|||||||
create type public.app_role as enum ('sa', 'admin', 'moderator', 'user');
|
create type public.app_role as enum ('sa', 'admin', 'moderator', 'user');
|
||||||
create type public.app_colors as enum ('red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose', 'black', 'white');
|
create type public.app_colors as enum ('red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose', 'black', 'white');
|
||||||
|
|
||||||
|
create type public.calendar_state as enum ('published', 'draft', 'archived');
|
||||||
|
|
||||||
-- DATA STRUCTURES
|
-- DATA STRUCTURES
|
||||||
-- Users
|
-- Users
|
||||||
create table public.users (
|
create table public.users (
|
||||||
@@ -44,12 +46,24 @@ create table public.worlds (
|
|||||||
);
|
);
|
||||||
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
||||||
|
|
||||||
|
-- World Players (join table)
|
||||||
|
create table public.world_players (
|
||||||
|
world_id bigint references public.worlds on delete cascade,
|
||||||
|
player_id uuid references public.users on delete cascade,
|
||||||
|
joined_at timestamptz default now(),
|
||||||
|
primary key (world_id, player_id)
|
||||||
|
);
|
||||||
|
comment on table public.world_players is 'Players (users) that belong to specific worlds';
|
||||||
|
comment on column public.world_players.joined_at is 'Timestamp when the player joined the world';
|
||||||
|
|
||||||
-- World Calendars
|
-- World Calendars
|
||||||
create table public.calendars (
|
create table public.calendars (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
|
short_id text unique,
|
||||||
name text not null,
|
name text not null,
|
||||||
today json not null,
|
today json not null,
|
||||||
color app_colors default 'black',
|
color app_colors default 'black',
|
||||||
|
state calendar_state default 'draft',
|
||||||
world_id bigint references public.worlds on delete cascade not null
|
world_id bigint references public.worlds on delete cascade not null
|
||||||
);
|
);
|
||||||
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
|
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
|
||||||
@@ -187,6 +201,10 @@ create policy "Allow GMs to edit their worlds" on public.worlds for update using
|
|||||||
create policy "Allow GMs to delete their worlds" on public.worlds for delete using ( auth.uid() = gm_id );
|
create policy "Allow GMs to delete their worlds" on public.worlds for delete using ( auth.uid() = gm_id );
|
||||||
|
|
||||||
-- Calendar policies
|
-- Calendar policies
|
||||||
|
create policy "Allow anonymous access to published calendars" on public.calendars
|
||||||
|
for select
|
||||||
|
using (state = 'published');
|
||||||
|
|
||||||
create policy "Allow GMs to see their calendars" on public.calendars for select using (
|
create policy "Allow GMs to see their calendars" on public.calendars for select using (
|
||||||
exists (
|
exists (
|
||||||
select 1 from worlds
|
select 1 from worlds
|
||||||
@@ -213,6 +231,17 @@ create policy "Allow GMs to delete their calendars" on public.calendars for dele
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Month policies
|
-- Month policies
|
||||||
|
create policy "Allow anonymous access to months in published calendars" ON public.calendar_months
|
||||||
|
for select
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars
|
||||||
|
where calendars.id = calendar_months.calendar_id
|
||||||
|
and calendars.state = 'published'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
create policy "Allow GMs to see their calendar's months"
|
create policy "Allow GMs to see their calendar's months"
|
||||||
on public.calendar_months
|
on public.calendar_months
|
||||||
for select
|
for select
|
||||||
@@ -267,6 +296,17 @@ create policy "Allow GMs to delete their calendar's months"
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Event policies
|
-- Event policies
|
||||||
|
create policy "Allow anonymous access to non-hidden events in published calendars" ON public.calendar_events
|
||||||
|
for select
|
||||||
|
using (
|
||||||
|
not hidden and exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars
|
||||||
|
where calendars.id = calendar_events.calendar_id
|
||||||
|
and calendars.state = 'published'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
create policy "Allow GMs to see their events"
|
create policy "Allow GMs to see their events"
|
||||||
on public.calendar_events
|
on public.calendar_events
|
||||||
for select
|
for select
|
||||||
@@ -342,8 +382,8 @@ create policy "Allow individual update access for GMs" on public.characters for
|
|||||||
|
|
||||||
-- Categories are public to view but not to insert
|
-- Categories are public to view but not to insert
|
||||||
-- Needs to be refactored maybe, if in the future we want a default set AND user defined ones
|
-- Needs to be refactored maybe, if in the future we want a default set AND user defined ones
|
||||||
create policy "Allow logged-in read access" on public.calendar_event_categories for select using ( auth.role() = 'authenticated' );
|
create policy "Allow all read access" on public.calendar_event_categories for select to authenticated, anon using ( true );
|
||||||
create policy "Allow logged-in read access" on public.calendar_event_categories_links for select using ( auth.role() = 'authenticated' );
|
create policy "Allow all read access" on public.calendar_event_categories_links for select to authenticated, anon using ( true );
|
||||||
create policy "Allow logged-in read access" on public.character_categories for select using ( auth.role() = 'authenticated' );
|
create policy "Allow logged-in read access" on public.character_categories for select using ( auth.role() = 'authenticated' );
|
||||||
create policy "Allow logged-in read access" on public.character_categories_links for select using ( auth.role() = 'authenticated' );
|
create policy "Allow logged-in read access" on public.character_categories_links for select using ( auth.role() = 'authenticated' );
|
||||||
|
|
||||||
@@ -391,3 +431,48 @@ begin
|
|||||||
return user_id;
|
return user_id;
|
||||||
end;
|
end;
|
||||||
$$ language plpgsql;
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
-- Add short IDs to calendars
|
||||||
|
-- Function to generate YouTube-like IDs
|
||||||
|
CREATE OR REPLACE FUNCTION public.generate_short_id()
|
||||||
|
RETURNS text AS $$
|
||||||
|
DECLARE
|
||||||
|
characters text := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||||
|
result text := '';
|
||||||
|
i integer;
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 1..11 LOOP
|
||||||
|
result := result || substr(characters, floor(random() * length(characters) + 1)::integer, 1);
|
||||||
|
END LOOP;
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
-- Trigger function to set unique short ID
|
||||||
|
CREATE OR REPLACE FUNCTION public.set_calendar_short_id()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
new_id text;
|
||||||
|
attempts integer := 0;
|
||||||
|
BEGIN
|
||||||
|
LOOP
|
||||||
|
new_id := generate_short_id();
|
||||||
|
BEGIN
|
||||||
|
NEW.short_id = new_id;
|
||||||
|
RETURN NEW;
|
||||||
|
EXCEPTION WHEN unique_violation THEN
|
||||||
|
attempts := attempts + 1;
|
||||||
|
IF attempts > 5 THEN
|
||||||
|
RAISE EXCEPTION 'Could not generate unique ID after 5 attempts';
|
||||||
|
END IF;
|
||||||
|
CONTINUE;
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Create trigger
|
||||||
|
CREATE TRIGGER set_calendar_short_id
|
||||||
|
BEFORE INSERT ON calendars
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.set_calendar_short_id();
|
||||||
|
|||||||
Reference in New Issue
Block a user