Compare commits

...

20 Commits

Author SHA1 Message Date
05d11e0ea5
refactor(api): change stuff in story updating route
if the story doesn't exist/is null, throw
ensure chapter word count is updated (since we weren't already doing that for some reason....)
instead of clearing the original story's chapters and re-adding them one by one, update the existing ones in place, otherwise push to the array
use new signature for `replaceOrUploadContent` call
ensure chapters are sorted
2024-07-09 21:10:03 -04:00
d030346dde
fix(components): replace console.error -> console.warn 2024-07-09 20:54:30 -04:00
6016813f4c
refactor(components): update story form
add `draftData` prop to distinguish whether we're editing a draft or a published story
actually renumber chapter indexes after reordering
use draftData prop in form change callback
2024-07-09 20:53:50 -04:00
4838b7b624
fix(components): un-scope story list styles 2024-07-09 20:40:24 -04:00
5c6cb84383
refactor(components): sidebar looks and acts prettier now 2024-07-09 20:39:49 -04:00
25b7e723f6
refactor(api): actually use doNotSelect filter when querying user at login 2024-07-09 20:36:41 -04:00
609562b7fa
refactor(client-side): make list actions async 2024-07-09 20:33:20 -04:00
a7b8c07952
refactor(components): navbar updates
add `key` attribute to `a-menu` element
only show "new story" button to logged in users
change `data` check to check for `data.user`
2024-07-09 20:31:53 -04:00
e28f6b6974
refactor(pages): add title to login page 2024-07-09 20:23:03 -04:00
af1e08227d
refactor(pages): remove top margin from home page 2024-07-09 20:20:10 -04:00
abbdc61e79
chore(dependencies): bump everything 2024-07-09 20:18:16 -04:00
1a3135c6c4
refactor(pages, components): rearrange layout
sidebars now open beside the actual page content instead of beside the body
2024-04-20 20:59:30 -04:00
05a20ff94e
fix(pages, components): fix inconsistent theme when logging in/out
manually set body's `data-theme` attribute to the appropriate value
2024-04-20 20:56:47 -04:00
c68762ceac
chore(dependencies): update tinymce related dependencies
add @tinymce/tinymce-vue, remove base tinymce package [we have a submodule for that ;)]
2024-04-20 19:31:07 -04:00
bed12e2eee
chore(dependencies): upgrade nuxt to latest version
also change nuxt-speedkit -> nuxt-booster
2024-04-20 19:28:57 -04:00
bb6d05be72
refactor(components): update icon component
merge styles passed in from a parent component inline
2024-04-20 19:22:11 -04:00
7932152025
refactor(db/models): re-introduce chapter index field
turns out we need it after all..
2024-04-20 19:17:43 -04:00
a88f418901
refactor(server/utils): update server-side constants
define array of sensitive user model fields to exclude when querying and returning
2024-04-02 01:33:15 -04:00
0d6acdf174
refactor(workspace): update tsconfig files
enable `esmoduleinterop` and `allowsyntheticdefaultimports`
2024-04-02 01:21:13 -04:00
25582dd1f1
fix(pages): update bands page
make "subscribe" or "unsubscribe" buttons update the ui immediately
2024-04-02 01:16:34 -04:00
19 changed files with 264 additions and 200 deletions

@ -24,7 +24,8 @@
<template> <template>
<i <i
:style="{ :style="{
fontSize: `${size}${propUnit}`, ...$attrs.style,
fontSize: pixi,
color: icolor || 'currentcolor', color: icolor || 'currentcolor',
}" }"
:class="`fa${styleMap[istyle]} fa-${name}`" :class="`fa${styleMap[istyle]} fa-${name}`"

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Grid } from "ant-design-vue"; import { Grid, ItemType } from "ant-design-vue";
const bp = Grid.useBreakpoint(); const bp = Grid.useBreakpoint();
const { data, status } = useAuth(); const { data, status } = useAuth();
const itemMap = ref({ const itemMap = ref({
@ -22,6 +22,7 @@
let selected: string[] = [cur.value]; let selected: string[] = [cur.value];
const clickFn = (minfo) => { const clickFn = (minfo) => {
console.log("clicky", minfo);
if (itemMap.value[minfo.key] === undefined) return; if (itemMap.value[minfo.key] === undefined) return;
cur.value = itemMap.value[minfo.key]; cur.value = itemMap.value[minfo.key];
selected = [cur.value]; selected = [cur.value];
@ -43,12 +44,13 @@
}" }"
@click="clickFn" @click="clickFn"
:active-key="cur" :active-key="cur"
:key="data?.user?._id"
> >
<a-menu-item key="home"> Home </a-menu-item> <a-menu-item key="home"> Home </a-menu-item>
<a-menu-item key="bands"> Bands </a-menu-item> <a-menu-item key="bands"> Bands </a-menu-item>
<a-menu-item key="authors"> Authors </a-menu-item> <a-menu-item key="authors"> Authors </a-menu-item>
<a-menu-item key="forum"> Message Board</a-menu-item> <a-menu-item key="forum"> Message Board</a-menu-item>
<a-sub-menu title="My Stuff" v-if="!!data?.user" key="group/my-stuff"> <a-sub-menu :disabled="!data?.user" v-if="!!data?.user" title="My Stuff" key="group/my-stuff">
<a-menu-item key="account"> Account </a-menu-item> <a-menu-item key="account"> Account </a-menu-item>
<a-menu-item key="edit-profile"> Edit Profile </a-menu-item> <a-menu-item key="edit-profile"> Edit Profile </a-menu-item>
<a-menu-item key="profile"> View Profile </a-menu-item> <a-menu-item key="profile"> View Profile </a-menu-item>
@ -60,9 +62,9 @@
<a-menu-item key="admin" v-if="data?.user?.profile.isAdmin || false"> Admin </a-menu-item> <a-menu-item key="admin" v-if="data?.user?.profile.isAdmin || false"> Admin </a-menu-item>
<a-menu-item key="logout" v-if="!!data?.user"> Logout </a-menu-item> <a-menu-item key="logout" v-if="!!data?.user"> Logout </a-menu-item>
</a-menu> </a-menu>
<div> <div v-if="data?.user">
<nuxt-link to="/new-story"> <nuxt-link to="/new-story">
<a-button v-if="data?.user" type="primary" tooltip="Post a New Story"> <a-button type="primary" tooltip="Post a New Story">
<!-- <template #icon> <!-- <template #icon>
</template> --> </template> -->
<icon istyle="regular" name="file-plus" /> <icon istyle="regular" name="file-plus" />
@ -70,7 +72,7 @@
</a-button> </a-button>
</nuxt-link> </nuxt-link>
</div> </div>
<div class="acbut" v-if="!data"> <div class="acbut" v-if="!data?.user">
<a-button size="large" @click="() => navigateTo('/auth/login')"> Login </a-button> <a-button size="large" @click="() => navigateTo('/auth/login')"> Login </a-button>
<a-button size="large" type="primary" @click="() => navigateTo('/auth/register')"> Register </a-button> <a-button size="large" type="primary" @click="() => navigateTo('/auth/register')"> Register </a-button>
</div> </div>

@ -3,6 +3,7 @@
import { ItemType, theme } from "ant-design-vue"; import { ItemType, theme } from "ant-design-vue";
import Icon from "../icon.vue"; import Icon from "../icon.vue";
import { ISidebarItem } from "@models/sidebarEntry"; import { ISidebarItem } from "@models/sidebarEntry";
import { NuxtLink } from "#components";
const { useToken } = theme; const { useToken } = theme;
const { token } = useToken(); const { token } = useToken();
@ -10,8 +11,8 @@
const selState = ref<string>(""); const selState = ref<string>("");
const { data: injecto } = await useApiFetch<ISidebarItem[]>("/sidebar"); const { data: injecto } = await useApiFetch<ISidebarItem[]>("/sidebar");
let custItems = computed(() => (injecto.value || ([] as ISidebarItem[])).sort((a, b) => a.index - b.index));
let items = reactive<ItemType[]>([ /*let items = ref<ItemType[]>([
{ {
key: "important", key: "important",
label: h("span", { class: "smallcaps" }, ["Pinned"]), label: h("span", { class: "smallcaps" }, ["Pinned"]),
@ -57,27 +58,24 @@
name: "sparkles", name: "sparkles",
size: 19, size: 19,
}), }),
children: (injecto.value || ([] as ISidebarItem[])) children: custItems.value.map((b) => ({
.sort((a, b) => a.index - b.index) key: b.url,
.map((b) => ({ label: h(
key: b.url, "span",
label: h( {
"span", "data-color": b.color,
{ class: "custom-side-item",
"data-color": b.color, },
class: "custom-side-item", [h(NuxtLink, { to: b.url }, [b.linkTitle])],
}, ),
[h(NuxtLink, { to: b.url }, [b.linkTitle])], })),
),
})),
} as SubMenuType, } as SubMenuType,
]); ]);*/
// console.log("wtf", items)
</script> </script>
<template> <template>
<!-- <client-only>--> <!-- <client-only>-->
<a-menu <a-menu
id="sidebar-menu"
mode="inline" mode="inline"
@select=" @select="
({ item, key, selectedKeys }) => { ({ item, key, selectedKeys }) => {
@ -89,37 +87,41 @@
" "
:trigger-sub-menu-action="'click'" :trigger-sub-menu-action="'click'"
v-model:active-key="selState" v-model:active-key="selState"
:items="items"
:inline-indent="16" :inline-indent="16"
> >
<!-- <a-sub-menu> <a-sub-menu key="important">
<template #title> <template #icon>
<sidebar-icon> <icon istyle="regular" name="thumbtack" :size="19" />
<template #icon> </template>
<Icon name="sparkles" istyle="regular" :size="19"/> <template #title>
</template> <span class="smallcaps">Pinned</span>
<template #rest> </template>
<div class="smallcaps"> <a-menu-item key="/submission-rules">
fun features <template #icon>
</div> <icon istyle="regular" name="memo" :size="15" />
</template>
</sidebar-icon>
</template> </template>
<a-menu-item> <b :style="{ color: token.colorInfo }">SUBMISSION RULES</b>
<sidebar-icon> </a-menu-item>
<template #icon> <a-menu-item key="/terms">
<icon name="memo" :icolor="token.colorInfo" istyle="regular" :size="13"/> <template #icon>
</template> <icon istyle="regular" name="globe" :size="15" />
<template #rest> </template>
<nuxt-link to="/submission-rules"> <b>Terms of Service</b>
<b :style="{ color: token.colorInfo }"> </a-menu-item>
submission rules </a-sub-menu>
</b> <a-sub-menu key="fun-features">
</nuxt-link> <template #icon>
</template> <Icon name="sparkles" istyle="regular" :size="19" />
</sidebar-icon> </template>
</a-menu-item> <template #title>
</a-sub-menu> --> <div class="smallcaps">fun features</div>
</template>
<a-menu-item v-for="item in custItems" :key="item.url">
<span :style="{ color: item.color }">
<nuxt-link :to="item.url">{{ item.linkTitle }}</nuxt-link>
</span>
</a-menu-item>
</a-sub-menu>
</a-menu> </a-menu>
<!-- </client-only>--> <!-- </client-only>-->
</template> </template>
@ -127,4 +129,7 @@
.smallcaps { .smallcaps {
font-variant: small-caps; font-variant: small-caps;
} }
#sidebar-menu ul {
height: 100%;
}
</style> </style>

@ -52,8 +52,8 @@
/> />
</div> </div>
</template> </template>
<style scoped> <style>
.ant-list-items > * + * { ul.ant-list-items > * + * {
margin-top: 1.2em; margin-top: 1.2em;
} }

@ -5,7 +5,7 @@
import { Field, FieldArray, useForm, useFieldArray } from "vee-validate"; import { Field, FieldArray, useForm, useFieldArray } from "vee-validate";
import { ASpin } from "#components"; import { ASpin } from "#components";
import { storySchema } from "@client/storyFormSchema"; import { storySchema } from "@client/storyFormSchema";
import { FormStory, defaultChapter } from "@client/types/form/story"; import { FormStory, defaultChapter, FormChapter } from "@client/types/form/story";
import { autoEdit, autoSave, debouncedAutoEdit, debouncedAutoSave } from "@client/utils"; import { autoEdit, autoSave, debouncedAutoEdit, debouncedAutoSave } from "@client/utils";
import findUser from "~/components/findUser.vue"; import findUser from "~/components/findUser.vue";
@ -20,17 +20,16 @@
endpoint: string; endpoint: string;
endpointMethod: "put" | "post"; endpointMethod: "put" | "post";
submitText?: string; submitText?: string;
draftData?: {
endpoint: string;
endpointMethod: "put" | "post";
};
}>(); }>();
let w;
onMounted(() => {
w = window;
});
const dc = defaultChapter; const dc = defaultChapter;
// data: FormStory;
const sdata = defineModel<FormStory>("data", { const sdata = defineModel<FormStory>("data", {
required: true, required: true,
}); });
let drag = false; let drag: boolean = false;
const expandos = ref<string[]>([]); const expandos = ref<string[]>([]);
function logSubmit(values, actions) { function logSubmit(values, actions) {
@ -46,27 +45,32 @@
otherBtnInvoked.value = false; otherBtnInvoked.value = false;
await autoSave(values); await autoSave(values);
} else { } else {
const { data: dat } = await useApiFetch(`/story/new`, { const { data: dat } = await useApiFetch<any>(`/story/new`, {
method: "post", method: "post",
body: values, body: values,
}); });
if (dat.success) { if (dat.value.success) {
await router.push(`/story/${dat.story._id}/1`); await router.push(`/story/${dat.value.story._id}/1`);
} }
} }
} else { } else {
await autoEdit(values, props.endpoint, props.endpointMethod); await autoEdit(values, props.endpoint, props.endpointMethod);
} }
} }
function inval({ values, errors, results }) {
logSubmit(values, undefined);
}
const { values, setFieldValue, handleSubmit } = useForm<FormStory>({ const { values, setFieldValue, handleSubmit } = useForm<FormStory>({
keepValuesOnUnmount: true, keepValuesOnUnmount: true,
validationSchema: storySchema, validationSchema: storySchema,
initialValues: sdata.value, initialValues: sdata.value,
}); });
// const { push, remove, move, fields } = useFieldArray<FormChapter>("chapters"); function renumber(update: (idx: number, value: FormChapter) => void) {
for (let i = 0; i < values.chapters.length; i++) {
const nv = values.chapters[i];
nv.index = i + 1;
update(i, nv);
sdata.value.chapters[i].index = i + 1;
}
}
const subCb = handleSubmit(onSubmit); const subCb = handleSubmit(onSubmit);
const pushHOF = (push) => (e) => { const pushHOF = (push) => (e) => {
@ -96,7 +100,18 @@
}; };
</script> </script>
<template> <template>
<form data-testid="storyform" @submit="subCb" @change="() => (canDraft ? debouncedAutoSave(values) : debouncedAutoEdit(values, endpoint, endpointMethod))"> <form
data-testid="storyform"
@submit="subCb"
@change="
() =>
canDraft
? draftData
? debouncedAutoSave(values, draftData.endpoint, draftData.endpointMethod)
: debouncedAutoSave(values)
: debouncedAutoEdit(values, endpoint, endpointMethod)
"
>
<!-- <a-form v-bind:model="acData"> --> <!-- <a-form v-bind:model="acData"> -->
<Field name="title" v-slot="{ value, field, errorMessage }"> <Field name="title" v-slot="{ value, field, errorMessage }">
@ -112,30 +127,26 @@
</Field> </Field>
<a-divider /> <a-divider />
<!-- <test1/> --> <!-- <test1/> -->
<field-array name="chapters" v-slot="{ fields, push, remove, move }"> <field-array name="chapters" v-slot="{ fields, push, remove, move, update }">
<client-only :fallback="h(ASpin)"> <client-only :fallback="h(ASpin)">
<div> <div>
<div v-for="(element, index) in data.chapters"> <div v-for="(element, index) in values.chapters">
<client-only> <client-only>
<Teleport :to="`#chapter-\\[${element.uuidKey}\\]`"> <Teleport :to="`#chapter-\\[${element.uuidKey}\\]`">
<a-collapse v-model:active-key="expandos" collapsible="icon"> <a-collapse v-model:active-key="expandos" collapsible="icon">
<template #expandIcon="{ isActive }"> <template #expandIcon="{ isActive }">
<span :data-testid="`storyform.chapters[${index}].collapse`"> <RightOutlined :rotate="isActive ? 90 : undefined" /></span> <span :data-testid="`storyform.chapters[${index}].collapse`"> <RightOutlined :rotate="isActive ? 90 : undefined" /></span>
</template> </template>
<a-collapse-panel :key="`${element.uuidKey}`"> <a-collapse-panel :key="`${element.uuidKey}`" :data-testid="`storyform.chapters[${index}].outer`">
<template #header> <template #header>
<div :data-testid="`storyform.chapters[${index}].header`" style="display: flex; justify-content: space-between"> <div :data-testid="`storyform.chapters[${index}].header`" style="display: flex; align-items: center; justify-content: space-between">
<span :data-testid="`storyform.chapters[${index}].titleEl`">{{ values.chapters[index]?.chapterTitle || "Untitled" }}</span> <span :data-testid="`storyform.chapters[${index}].titleEl`">{{ values.chapters[index]?.chapterTitle || "Untitled" }}</span>
<a-button <a-button
@click=" @click="
(e) => { (e) => {
// let localFields = toRaw(fields);
// log.debug(`${index} | ${element.index}`);
// log.debug('fields->', localFields);
data.chapters.splice(index, 1); data.chapters.splice(index, 1);
remove(index); remove(index);
// todo renumber renumber(update);
// renumber()
} }
" "
> >
@ -172,6 +183,7 @@
console.debug(e.moved); console.debug(e.moved);
move(e.moved.oldIndex, e.moved.newIndex); move(e.moved.oldIndex, e.moved.newIndex);
data.chapters = lmove(data.chapters, e.moved.oldIndex, e.moved.newIndex); data.chapters = lmove(data.chapters, e.moved.oldIndex, e.moved.newIndex);
renumber(update);
// w.tinymce.remove(); // w.tinymce.remove();
// log.debug(toRaw(acData.chapters.map((a) => toRaw(a)))); // log.debug(toRaw(acData.chapters.map((a) => toRaw(a))));
} }
@ -191,3 +203,11 @@
<a-button html-type="submit" v-if="canDraft" @click="() => (otherBtnInvoked = true)"> Save for Later </a-button> <a-button html-type="submit" v-if="canDraft" @click="() => (otherBtnInvoked = true)"> Save for Later </a-button>
</form> </form>
</template> </template>
<style>
.ant-collapse-item > .ant-collapse-header {
align-items: center !important;
}
.ant-collapse-header.ant-collapse-icon-collapsible-only {
align-items: center !important;
}
</style>

@ -39,7 +39,7 @@
console.log(fileField.value); console.log(fileField.value);
} }
} catch (e) { } catch (e) {
console.error('not yet'); console.warn('not yet');
} }
} }
" "

@ -50,67 +50,76 @@
<i> Nothing here but crickets. </i> <i> Nothing here but crickets. </i>
</template> </template>
<a-layout class="ylayout"> <a-layout class="ylayout">
<a-layout-header class="alayhead"> <a-layout>
<div style="display: flex; align-items: center; flex-wrap: wrap"> <a-layout-header class="alayhead" :style="{ backgroundColor: darkBool ? '#141414' : '#f5f5f5' }">
<div class="siteTitle">Rockfic</div> <div style="display: flex; align-items: center; flex-wrap: wrap">
<div class="stat-block"> <div class="siteTitle">Rockfic</div>
<div> <div class="stat-block">
<a-typography-text> Band fiction that rocks </a-typography-text> <div>
<a-typography-text type="secondary"> With {{ totals?.stories || 0 }} stories by {{ totals?.authors || 0 }} authors </a-typography-text> <a-typography-text> Band fiction that rocks </a-typography-text>
</div> <a-typography-text type="secondary"> With {{ totals?.stories || 0 }} stories by {{ totals?.authors || 0 }} authors </a-typography-text>
</div>
<navbar v-if="!!bp['md']" :inline="false" />
</div>
<a-button class="mobileTrigger" type="primary" @click="() => (collapsed = !collapsed)" v-if="!bp['md']">
<menu-unfold-outlined v-if="nav" />
<menu-fold-outlined v-else />
</a-button>
</a-layout-header>
<a-layout class="mlayout" has-sider style="padding-top: 1em">
<a-layout-sider :trigger="null" :collapsed="true" :collapsed-width="0" :collapsible="true" v-model:collapsed="collapsed" v-if="!bp['md']">
<navbar inline />
</a-layout-sider>
<a-layout-content style="flex-grow: 1">
<slot />
</a-layout-content>
<a-layout-sider
:zero-width-trigger-style="{
background: '#e92662',
padding: '1.2em',
position: 'fixed',
right: 0,
borderRadius: '15%',
color: 'white',
border: '2.4px solid #fffFFF80',
top: '75vh',
'z-index': 99999999,
}"
:theme="darko ? 'dark' : 'light'"
:breakpoint="'lg'"
v-model:collapsed="collapsed"
:collapsible="true"
:collapsed-width="0"
:style="{
color: col,
height: '100%',
position: 'fixed',
right: '0px',
borderLeft: `2px solid ${darko ? '#fff' : '#ccc'}`,
}"
>
<sidebar-thing />
<template #trigger>
<div class="outerst" @click="() => (collapsed = !collapsed)">
<div :class="sideTriggerVal">
<icon istyle="solid" name="chevron-right" :size="30" />
</div> </div>
</div> </div>
</template> <navbar v-if="!!bp['md']" :inline="false" />
</a-layout-sider> </div>
<a-button class="mobileTrigger" type="primary" @click="() => (nav = !nav)" v-if="!bp['md']">
<menu-unfold-outlined v-if="nav" />
<menu-fold-outlined v-else />
</a-button>
</a-layout-header>
<a-layout class="mlayout" has-sider style="padding-top: 1em">
<a-layout-sider
:trigger="null"
:theme="darkBool ? 'dark' : 'light'"
:collapsed="nav"
:collapsed-width="0"
:collapsible="true"
v-model:collapsed="collapsed"
v-if="!bp['md']"
>
<navbar inline />
</a-layout-sider>
<a-layout-content style="flex-grow: 1">
<slot />
</a-layout-content>
<a-layout-sider
:zero-width-trigger-style="{
background: '#e92662',
padding: '1.2em',
position: 'fixed',
right: 0,
borderRadius: '15%',
color: 'white',
border: '2.4px solid #fffFFF80',
top: '75vh',
'z-index': 99999999,
}"
:theme="darko ? 'dark' : 'light'"
:breakpoint="'lg'"
v-model:collapsed="collapsed"
:collapsible="true"
:collapsed-width="0"
:style="{
color: col,
right: '-3px',
alignSelf: 'stretch',
borderLeft: `2px solid ${darko ? '#fff' : '#ccc'}`,
}"
>
<sidebar-thing />
<template #trigger>
<div class="outerst" @click="() => (collapsed = !collapsed)">
<div :class="sideTriggerVal">
<icon istyle="solid" name="chevron-right" :size="30" />
</div>
</div>
</template>
</a-layout-sider>
</a-layout>
<a-layout-footer style="bottom: 100%">
<cfooter />
</a-layout-footer>
</a-layout> </a-layout>
<a-layout-footer style="bottom: 100%">
<cfooter />
</a-layout-footer>
</a-layout> </a-layout>
<!-- <div class="mlayout"> <!-- <div class="mlayout">
<a-skeleton/> <a-skeleton/>
@ -137,7 +146,7 @@
.sideWrap { .sideWrap {
height: 100vh; height: 100vh;
right: -1rem; right: -3rem;
width: inherit; width: inherit;
/* display: flex; /* display: flex;
flex-direction: column; flex-direction: column;

@ -4,14 +4,14 @@ import { useRoute, useRouter } from "#app";
const base = `/user/me`; const base = `/user/me`;
export const favourites = (values: (any & { _id: number })[], id: number, remove: boolean, type: "story" | "author") => { export const favourites = async (values: (any & { _id: number })[], id: number, remove: boolean, type: "story" | "author") => {
values?.splice( values?.splice(
values!.findIndex((a) => a._id == id), values!.findIndex((a) => a._id == id),
1, 1,
); );
const key = type === "story" ? "stories" : "authors"; const key = type === "story" ? "stories" : "authors";
const todo = [id]; const todo = [id];
useApiFetch(`${base}/favs`, { await useApiFetch(`${base}/favs`, {
method: "put", method: "put",
body: { body: {
[key]: { [key]: {
@ -22,7 +22,7 @@ export const favourites = (values: (any & { _id: number })[], id: number, remove
}); });
}; };
export const subscriptions = ( export const subscriptions = async (
values: ((any & { _id: number }) | number)[], values: ((any & { _id: number }) | number)[],
id: number, id: number,
action: "hide" | "subscribe" | "unsubscribe", action: "hide" | "subscribe" | "unsubscribe",
@ -36,7 +36,7 @@ export const subscriptions = (
values!.findIndex((a) => a._id == id || a == id), values!.findIndex((a) => a._id == id || a == id),
1, 1,
); );
useApiFetch(`${base}/${action}`, { await useApiFetch(`${base}/${action}`, {
body: { body: {
push: { push: {
[type]: [id], [type]: [id],
@ -46,7 +46,7 @@ export const subscriptions = (
method: "put", method: "put",
}); });
} else if (action == "subscribe") { } else if (action == "subscribe") {
useApiFetch(`${base}/subscriptions`, { await useApiFetch(`${base}/subscriptions`, {
body: { body: {
push: { push: {
[type]: [id], [type]: [id],
@ -56,7 +56,7 @@ export const subscriptions = (
method: "put", method: "put",
}); });
} else if (action == "unsubscribe") { } else if (action == "unsubscribe") {
useApiFetch(`${base}/subscriptions`, { await useApiFetch(`${base}/subscriptions`, {
body: { body: {
pull: { pull: {
[type]: [id], [type]: [id],

@ -142,3 +142,4 @@ export const rc: RuntimeConfig & any = {
}, },
app: {}, app: {},
}; };
export const doNotSelect = ["password", "auth", "ipLog"].map((b) => `-${b}`).join(" ");

@ -4,7 +4,7 @@ export interface IChapter {
title: string; title: string;
summary: string; summary: string;
id?: number; id?: number;
// index: number; index: number;
words?: number; words?: number;
notes: string; notes: string;
genre: string[]; genre: string[];
@ -25,6 +25,9 @@ export const Chapter = new Schema<IChapter>({
id: { id: {
type: Number, type: Number,
}, },
index: {
type: Number,
},
summary: { summary: {
type: String, type: String,
}, },

@ -4,8 +4,9 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev --host 127.0.0.1",
"generate": "nuxt generate", "generate": "nuxt generate",
"postinstall": "nuxt prepare",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
@ -13,17 +14,27 @@
"@nuxt/devtools": "latest", "@nuxt/devtools": "latest",
"@nuxtjs/i18n": "^8.0.0-rc.9", "@nuxtjs/i18n": "^8.0.0-rc.9",
"@types/uuid": "^9.0.4", "@types/uuid": "^9.0.4",
"@vue/language-server": "^1.8.27", "@vitejs/plugin-vue": "^5.0.2",
"nuxt": "^3.9.0", "@vitest/browser": "^1.1.2",
"@vitest/ui": "^1.1.3",
"@vue/language-server": "latest",
"@vue/test-utils": "^2.4.3",
"happy-dom": "^12.10.3",
"jsdom": "^23.0.1",
"nuxt": "^3.12.3",
"playwright": "^1.40.1",
"playwright-core": "^1.40.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"tsconfig-to-dual-package": "^1.2.0", "ts-node": "^10.9.2",
"typescript": "latest" "typescript": "latest",
"unplugin-vue-components": "^0.26.0",
"vitest": "^1.4.0"
}, },
"dependencies": { "dependencies": {
"@ant-design-vue/nuxt": "^1.4.1", "@ant-design-vue/nuxt": "^1.4.1",
"@pinia/nuxt": "^0.4.11", "@pinia/nuxt": "^0.4.11",
"@tinymce/tinymce-vue": "latest",
"@sidebase/nuxt-auth": "0.7.0", "@sidebase/nuxt-auth": "0.7.0",
"@tinymce/tinymce-vue": "latest",
"@types/jsonwebtoken": "^9.0.3", "@types/jsonwebtoken": "^9.0.3",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/turndown": "^5.0.4", "@types/turndown": "^5.0.4",
@ -33,7 +44,7 @@
"axios": "^1.5.1", "axios": "^1.5.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"date-fns": "^2.30.0", "date-fns": "latest",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lodash-move": "^1.1.1", "lodash-move": "^1.1.1",
@ -44,13 +55,13 @@
"mongoose-sequence": "https://github.com/amansingh63/mongoose-sequence", "mongoose-sequence": "https://github.com/amansingh63/mongoose-sequence",
"nitropack": "^2.9.4", "nitropack": "^2.9.4",
"nuxi": "^3.10.0", "nuxi": "^3.10.0",
"nuxt-booster": "^3.0.0",
"nuxt-security": "^0.14.4", "nuxt-security": "^0.14.4",
"nuxt-speedkit": "3.0.0-next.26", "nuxt-speedkit": "3.0.0-next.26",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sanitize-html": "^2.11.0", "sanitize-html": "^2.11.0",
"sharp": "^0.33.2", "sharp": "^0.33.3",
"string-strip-html": "^13.4.3", "string-strip-html": "^13.4.3",
"tinymce": "^6.7.0",
"turndown": "^7.1.2", "turndown": "^7.1.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vee-validate": "^4.11.7", "vee-validate": "^4.11.7",

@ -12,22 +12,24 @@
}, },
middleware: ["auth"], middleware: ["auth"],
}); });
useHead({
title: "Log In",
});
const formState = reactive<FormState>({ const formState = reactive<FormState>({
username: "", username: "",
password: "", password: "",
}); });
const darkRef = inject<Ref<boolean>>("dark");
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
const { signIn, data } = useAuth(); const { signIn } = useAuth();
let reso: any;
try { try {
reso = await signIn(values); await signIn(values, { redirect: true, callbackUrl: "/" });
const { data } = useAuth();
await navigateTo({ darkRef.value = data.value.user.profile.nightMode;
path: "/", if (darkRef.value) document.body.dataset.theme = "dark";
}); await navigateTo("/");
} catch (e: any) { } catch (e: any) {
if (e.data) { if (e.data) {
notification["error"]({ notification["error"]({

@ -6,6 +6,9 @@
signOut({ signOut({
callbackUrl: "/", callbackUrl: "/",
}); });
const d = inject<Ref<boolean>>("dark");
d.value = false;
document.body.dataset.theme = undefined;
</script> </script>
<template> <template>
<a-typography-title :level="3"> Signed out. 👋 </a-typography-title> <a-typography-title :level="3"> Signed out. 👋 </a-typography-title>

@ -6,12 +6,18 @@
const { data: bands } = (await useApiFetch<NonNullable<IBand[]>>("/band/all")) as unknown as { data: Ref<IBand[]> }; const { data: bands } = (await useApiFetch<NonNullable<IBand[]>>("/band/all")) as unknown as { data: Ref<IBand[]> };
const { data: rd }: { data: any } = useAuth(); const {
data: { value: rd },
getSession,
} = useAuth();
await getSession({ force: true });
let inc = ref<number>(1);
const data = ref(rd);
const refresh = async () => { const refresh = async () => {
await useAuth().getSession({ force: true }); await useAuth().getSession({ force: true });
rd.value = useAuth().data.value; data.value = useAuth().data.value;
//inc.value += 1;
}; };
const hider = subscriptions; const hider = subscriptions;
if (bands.value == null) bands.value = []; if (bands.value == null) bands.value = [];
useHead({ useHead({
@ -21,7 +27,7 @@
<template> <template>
<a-list v-model:data-source="bands" :grid="bp"> <a-list v-model:data-source="bands" :grid="bp">
<template #renderItem="{ item }"> <template #renderItem="{ item }">
<a-list-item> <a-list-item :key="item._id + inc">
<a-row :gutter="[5, 5]"> <a-row :gutter="[5, 5]">
<a-col> <a-col>
<nuxt-link :to="`/band/${item._id}`"> <nuxt-link :to="`/band/${item._id}`">
@ -29,12 +35,12 @@
</nuxt-link> </nuxt-link>
</a-col> </a-col>
<!-- subscribe... --> <!-- subscribe... -->
<a-col v-if="rd && rd.user?._id" style="margin-left: auto"> <a-col v-if="data && data.user?._id" style="margin-left: auto">
<a <a
v-if="!rd?.user.subscriptions.bands.includes(item._id)" v-if="!data?.user.subscriptions.bands.includes(item._id)"
@click=" @click="
async (e) => { async (e) => {
hider(bands, item._id, 'subscribe', 'bands'); await hider(bands, item._id, 'subscribe', 'bands');
await refresh(); await refresh();
} }
" "
@ -45,7 +51,7 @@
v-else v-else
@click=" @click="
async (e) => { async (e) => {
hider(bands, item._id, 'unsubscribe', 'bands'); await hider(bands, item._id, 'unsubscribe', 'bands');
await refresh(); await refresh();
} }
" "
@ -53,11 +59,11 @@
<icon :istyle="'regular'" name="x" :size="12" /> <icon :istyle="'regular'" name="x" :size="12" />
</a> </a>
</a-col> </a-col>
<a-col v-if="rd?.user._id"> <a-col v-if="data?.user._id">
<a <a
@click=" @click="
async (e) => { async (e) => {
hider(bands, item._id, 'hide', 'bands'); await hider(bands, item._id, 'hide', 'bands');
await refresh(); await refresh();
} }
" "

@ -7,7 +7,7 @@
<template> <template>
<div> <div>
<a-typography-title :level="1"> The Latest Fics </a-typography-title> <a-typography-title :level="1" style="margin-top: 0"> The Latest Fics </a-typography-title>
<story-list :last="true" prefix="/latest" /> <story-list :last="true" prefix="/latest" />
</div> </div>
</template> </template>

@ -2,11 +2,12 @@ import mongoose from "mongoose";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { User } from "@models/user"; import { User } from "@models/user";
import { log } from "@server/logger"; import { log } from "@server/logger";
import { doNotSelect } from "@server/constants";
export default eventHandler(async (event) => { export default eventHandler(async (event) => {
const wrongMsg = "wrong credentials"; const wrongMsg = "wrong credentials";
let reqbody = await readBody(event); let reqbody = await readBody(event);
let user = await User.findOne({ username: reqbody.username }).exec(); let user = await User.findOne({ username: reqbody.username }).select(doNotSelect).exec();
// log.debug(reqbody, { label: "login/body" }); // log.debug(reqbody, { label: "login/body" });
// log.debug("USER -> " + user, { label: "login" }); // log.debug("USER -> " + user, { label: "login" });
// log.debug("conn ->" + mongoose.connection, { label: "login" }); // log.debug("conn ->" + mongoose.connection, { label: "login" });
@ -30,8 +31,9 @@ export default eventHandler(async (event) => {
} }
let tok = user.generateRefreshToken(useRuntimeConfig().jwt); let tok = user.generateRefreshToken(useRuntimeConfig().jwt);
// setCookie(event, "rockfic_cookie", tok); // setCookie(event, "rockfic_cookie", tok);
const fu = user.toObject();
return { return {
user, user: fu,
token: { token: {
refresh: tok, refresh: tok,
access: user.generateAccessToken(useRuntimeConfig().jwt), access: user.generateAccessToken(useRuntimeConfig().jwt),

@ -11,6 +11,12 @@ import { messages } from "@server/constants";
export default eventHandler(async (ev) => { export default eventHandler(async (ev) => {
let os: (Document<unknown, {}, IStory> & IStory) | null = await storyQuerier(ev); let os: (Document<unknown, {}, IStory> & IStory) | null = await storyQuerier(ev);
isLoggedIn(ev); isLoggedIn(ev);
if (!os) {
throw createError({
statusCode: 404,
message: messages[404],
});
}
if (!canModify(ev, os)) { if (!canModify(ev, os)) {
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
@ -23,39 +29,31 @@ export default eventHandler(async (ev) => {
completed: body.completed, completed: body.completed,
coAuthor: !!body.coAuthor ? body.coAuthor : null, coAuthor: !!body.coAuthor ? body.coAuthor : null,
}; };
for (const oc of os.chapters) {
let filename = `/stories/${oc.id}.txt`;
const bucket = getBucket();
const curs = bucket.find({ filename }).limit(1);
for await (const d of curs) {
await bucket.delete(d._id);
}
}
const cc = os.chapters;
os.chapters = [];
await os.save();
for (const c of body.chapters) { for (const c of body.chapters) {
let idx = cc.findIndex((k) => k.id === c.id); let idx = os.chapters.findIndex((k) => k.id === c.id);
const cont = await bodyHandler(c); const cont = await bodyHandler(c);
if (idx === -1) { if (idx === -1) {
os.chapters!.push({ os.chapters!.push({
...modelFormChapter(c), ...modelFormChapter(c),
words: countWords(cont),
posted: new Date(Date.now()), posted: new Date(Date.now()),
}); });
} else { } else {
os.chapters!.push({ os.chapters[idx] = {
...modelFormChapter(c), ...modelFormChapter(c),
// id: os.chapters[idx].id, id: os.chapters[idx].id,
words: countWords(cont), words: countWords(cont),
posted: cc[idx].posted, posted: os.chapters[idx].posted,
}); };
} }
} }
os.chapters.sort((a, b) => a.index - b.index);
await os.save(); await os.save();
for (let i = 0; i < os.chapters.length; i++) { for (let i = 0; i < os.chapters.length; i++) {
const c = os.chapters[i]; const c = os.chapters[i];
const cont = await bodyHandler(body.chapters[i]); const cont = await bodyHandler(body.chapters[i]);
await replaceOrUploadContent(c.id ?? c._id, cont); await replaceOrUploadContent(c.id ?? c._id, cont, getBucket());
} }
os = await Story.findOneAndUpdate( os = await Story.findOneAndUpdate(
{ {

@ -4,6 +4,8 @@
"allowJs": true, "allowJs": true,
"outDir": "../out", "outDir": "../out",
"noImplicitAny": false, "noImplicitAny": false,
"verbatimModuleSyntax": false "verbatimModuleSyntax": false,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
} }
} }

@ -1,14 +1,13 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json", "extends": "./.nuxt/tsconfig.json",
// https://nuxt.com/docs/guide/concepts/typescript
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"esModuleInterop": true,
"noImplicitAny": false, "noImplicitAny": false,
"noImplicitThis": false, "noImplicitThis": false,
"verbatimModuleSyntax": false "verbatimModuleSyntax": false,
// "paths": { "forceConsistentCasingInFileNames": false,
// "@dbconfig": ["./lib/dbconfig.ts"], "allowSyntheticDefaultImports": true
// "@functions": ["./lib/functions.ts"] },
// }
}
} }