♻️ Interactive v2 #1

Merged
LittleSheep merged 30 commits from refactor/v2 into master 2024-03-16 08:22:25 +00:00
16 changed files with 335 additions and 153 deletions
Showing only changes of commit 3300e46e88 - Show all commits

View File

@ -25,15 +25,15 @@ func createArticle(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
var data struct { var data struct {
Alias string `json:"alias"` Alias string `json:"alias" form:"alias"`
Title string `json:"title" validate:"required"` Title string `json:"title" form:"title" validate:"required"`
Description string `json:"description"` Description string `json:"description" form:"description"`
Content string `json:"content" validate:"required"` Content string `json:"content" form:"content" validate:"required"`
Hashtags []models.Tag `json:"hashtags"` Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories"` Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments"` Attachments []models.Attachment `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmID *uint `json:"realm_id"` RealmID *uint `json:"realm_id" form:"realm_id"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {
@ -78,14 +78,14 @@ func editArticle(c *fiber.Ctx) error {
id, _ := c.ParamsInt("articleId", 0) id, _ := c.ParamsInt("articleId", 0)
var data struct { var data struct {
Alias string `json:"alias" validate:"required"` Alias string `json:"alias" form:"alias" validate:"required"`
Title string `json:"title" validate:"required"` Title string `json:"title" form:"title" validate:"required"`
Description string `json:"description"` Description string `json:"description" form:"description"`
Content string `json:"content" validate:"required"` Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags"` Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories"` Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments"` Attachments []models.Attachment `json:"attachments" form:"attachments"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {

View File

@ -56,13 +56,13 @@ func createComment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
var data struct { var data struct {
Alias string `json:"alias"` Alias string `json:"alias" form:"alias"`
Content string `json:"content" validate:"required"` Content string `json:"content" form:"content" validate:"required"`
Hashtags []models.Tag `json:"hashtags"` PublishedAt *time.Time `json:"published_at" form:"published_at"`
Categories []models.Category `json:"categories"` Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Attachments []models.Attachment `json:"attachments"` Categories []models.Category `json:"categories" form:"categories"`
PublishedAt *time.Time `json:"published_at"` Attachments []models.Attachment `json:"attachments" form:"attachments"`
ReplyTo uint `json:"reply_to"` ReplyTo uint `json:"reply_to" form:"reply_to"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {
@ -133,12 +133,12 @@ func editComment(c *fiber.Ctx) error {
id, _ := c.ParamsInt("commentId", 0) id, _ := c.ParamsInt("commentId", 0)
var data struct { var data struct {
Alias string `json:"alias" validate:"required"` Alias string `json:"alias" form:"alias" validate:"required"`
Content string `json:"content" validate:"required"` Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags"` Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories"` Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments"` Attachments []models.Attachment `json:"attachments" form:"attachments"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {

View File

@ -25,14 +25,14 @@ func createMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
var data struct { var data struct {
Alias string `json:"alias"` Alias string `json:"alias" form:"alias"`
Content string `json:"content" validate:"required"` Content string `json:"content" form:"content" validate:"required"`
Hashtags []models.Tag `json:"hashtags"` Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories"` Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments"` Attachments []models.Attachment `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmID *uint `json:"realm_id"` RealmID *uint `json:"realm_id" form:"realm_id"`
RepostTo uint `json:"repost_to"` RepostTo uint `json:"repost_to" form:"repost_to"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {
@ -88,12 +88,12 @@ func editMoment(c *fiber.Ctx) error {
id, _ := c.ParamsInt("momentId", 0) id, _ := c.ParamsInt("momentId", 0)
var data struct { var data struct {
Alias string `json:"alias" validate:"required"` Alias string `json:"alias" form:"alias" validate:"required"`
Content string `json:"content" validate:"required"` Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags"` Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories"` Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments"` Attachments []models.Attachment `json:"attachments" form:"attachments"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {

View File

@ -97,8 +97,8 @@ func reactPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
var data struct { var data struct {
Symbol string `json:"symbol" validate:"required"` Symbol string `json:"symbol" form:"symbol" validate:"required"`
Attitude models.ReactionAttitude `json:"attitude" validate:"required"` Attitude models.ReactionAttitude `json:"attitude" form:"attitude" validate:"required"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {

View File

@ -80,7 +80,7 @@ func NewServer() {
posts.Post("/:postId/comments", authMiddleware, createComment) posts.Post("/:postId/comments", authMiddleware, createComment)
} }
moments := api.Group("/moments").Name("Moments API") moments := api.Group("/p/moments").Name("Moments API")
{ {
moments.Post("/", authMiddleware, createMoment) moments.Post("/", authMiddleware, createMoment)
moments.Put("/:momentId", authMiddleware, editMoment) moments.Put("/:momentId", authMiddleware, editMoment)

View File

@ -77,6 +77,7 @@ func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) {
table := viper.GetString("database.prefix") + v.TableName table := viper.GetString("database.prefix") + v.TableName
if err := v.Tx. if err := v.Tx.
Table(table). Table(table).
Select("*, ? as model_type", v.ColumnName).
Where("alias = ?", alias). Where("alias = ?", alias).
First(&item).Error; err != nil { First(&item).Error; err != nil {
return item, err return item, err

View File

@ -1,5 +1,6 @@
html, body, #app, .v-application { html, body, #app, .v-application {
overflow: auto !important; overflow: auto !important;
font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif;
} }
.no-scrollbar { .no-scrollbar {

View File

@ -5,20 +5,28 @@
<div v-else class="flex flex-col gap-2 mt-3"> <div v-else class="flex flex-col gap-2 mt-3">
<div v-for="(item, idx) in props.comments" class="text-sm"> <div v-for="(item, idx) in props.comments" class="text-sm">
<post-item :item="item" @update:item="val => updateItem(idx, val)" /> <post-item :item="item" @update:item="(val) => updateItem(idx, val)" />
</div> </div>
</div> </div>
<v-divider class="mt-2 mb-3 border-opacity-50 mx-[-1rem]" />
<v-btn block prepend-icon="mdi-pencil" variant="plain" @click="leaveComment">Leave your comment</v-btn>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { request } from "@/scripts/request" import { request } from "@/scripts/request"
import { reactive, ref } from "vue" import { reactive, ref, watch } from "vue"
import { useEditor } from "@/stores/editor"
import PostItem from "@/components/posts/PostItem.vue" import PostItem from "@/components/posts/PostItem.vue"
const editor = useEditor()
const props = defineProps<{ const props = defineProps<{
comments: any[] comments: any[]
model: any model: any
alias: any alias: any
item: any
}>() }>()
const emits = defineEmits(["update:comments"]) const emits = defineEmits(["update:comments"])
@ -50,8 +58,20 @@ async function readComments() {
readComments() readComments()
function updateItem(idx: number, data: any) { function updateItem(idx: number, data: any) {
const comments = JSON.parse(JSON.stringify(props.comments)); const comments = JSON.parse(JSON.stringify(props.comments))
comments[idx] = data; comments[idx] = data
emits("update:comments", comments); emits("update:comments", comments)
}
watch(editor, (val) => {
if (val.done) {
readComments().then(() => (val.done = false))
}
})
function leaveComment() {
editor.related.comment_to = props.item
editor.related.comment_to.model_type += "s"
editor.show.comment = true
} }
</script> </script>

View File

@ -12,9 +12,7 @@
<v-menu v-if="!props.readonly" location="bottom center"> <v-menu v-if="!props.readonly" location="bottom center">
<template v-slot:activator="{ props: binding }"> <template v-slot:activator="{ props: binding }">
<v-chip v-bind="binding" :size="props.size" prepend-icon="mdi-emoticon-plus"> <v-chip v-bind="binding" :size="props.size" prepend-icon="mdi-emoticon-plus"> React </v-chip>
React
</v-chip>
</template> </template>
<v-list density="compact" lines="one"> <v-list density="compact" lines="one">
@ -30,52 +28,53 @@
<v-snackbar v-model="status.added" :timeout="3000">Your react has been added into post.</v-snackbar> <v-snackbar v-model="status.added" :timeout="3000">Your react has been added into post.</v-snackbar>
<v-snackbar v-model="status.removed" :timeout="3000">Your react has been removed from post.</v-snackbar> <v-snackbar v-model="status.removed" :timeout="3000">Your react has been removed from post.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { request } from "@/scripts/request"; import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"; import { getAtk } from "@/stores/userinfo"
import { reactive, ref } from "vue"; import { reactive, ref } from "vue"
const emits = defineEmits(["update"]); const emits = defineEmits(["update"])
const props = defineProps<{ const props = defineProps<{
size?: string, size?: string
readonly?: boolean, readonly?: boolean
model: any, model: any
item: any, item: any
reactions: { [id: string]: number } reactions: { [id: string]: number }
}>(); }>()
const emojis: { [id: string]: { icon: string, attitude: number } } = { const emojis: { [id: string]: { icon: string; attitude: number } } = {
thumb_up: { icon: "👍", attitude: 1 }, thumb_up: { icon: "👍", attitude: 1 },
clap: { icon: "👏", attitude: 1 } clap: { icon: "👏", attitude: 1 }
};
function pickColor(): string {
const colors = ["blue", "green", "purple"];
const randomIndex = Math.floor(Math.random() * colors.length);
return colors[randomIndex];
} }
const status = reactive({ added: false, removed: false }); function pickColor(): string {
const error = ref<string | null>(null); const colors = ["blue", "green", "purple"]
const randomIndex = Math.floor(Math.random() * colors.length)
return colors[randomIndex]
}
const status = reactive({ added: false, removed: false })
const error = ref<string | null>(null)
async function reactPost(symbol: string, attitude: number) { async function reactPost(symbol: string, attitude: number) {
const res = await request(`/api/p/${props.model}/${props.item?.id}/react`, { const res = await request(`/api/p/${props.model}/${props.item?.id}/react`, {
method: "POST", method: "POST",
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" }, headers: { Authorization: `Bearer ${getAtk()}`, "Content-Type": "application/json" },
body: JSON.stringify({ symbol, attitude }) body: JSON.stringify({ symbol, attitude })
}); })
if (res.status === 201) { if (res.status === 201) {
status.added = true; status.added = true
emits("update", symbol, 1); emits("update", symbol, 1)
} else if (res.status === 204) { } else if (res.status === 204) {
status.removed = true; status.removed = true
emits("update", symbol, -1); emits("update", symbol, -1)
} else { } else {
error.value = await res.text(); error.value = await res.text()
} }
} }
</script> </script>

View File

@ -0,0 +1,65 @@
<template>
<v-card title="Leave your comment" :loading="loading">
<v-form @submit.prevent="postComment">
<v-card-text>
<v-textarea required hide-details name="content" variant="outlined" label="What do you want to say?" />
<p class="px-2 mt-1 text-body-2 opacity-80">Your comment will leave below {{ postIdentifier }}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="editor.show.comment = false">Cancel</v-btn>
<v-btn type="submit" :disabled="loading">Publish</v-btn>
</v-card-actions>
</v-form>
</v-card>
<v-snackbar v-model="success" :timeout="3000">Your comment has been published.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { computed, ref } from "vue"
const editor = useEditor()
const target = computed<any>(() => editor.related.comment_to)
const postIdentifier = computed(() => {
if (editor.related.comment_to?.title) {
return `${editor.related.comment_to.title}`
} else {
return `#${editor.related.comment_to?.alias}`
}
})
const error = ref<string | null>(null)
const success = ref(false)
const loading = ref(false)
async function postComment(evt: SubmitEvent) {
const data = new FormData(evt.target as HTMLFormElement)
if (!data.has("content")) return
loading.value = true
const res = await request(`/api/p/${target.value?.model_type}/${target.value?.alias}/comments`, {
method: "POST",
headers: { Authorization: `Bearer ${getAtk()}` },
body: data
})
if (res.status === 200) {
success.value = true
} else {
error.value = await res.text()
}
loading.value = false
editor.show.comment = false
editor.done = true
}
</script>

View File

@ -0,0 +1,123 @@
<template>
<v-card title="Record a moment" :loading="loading">
<v-form @submit.prevent="postMoment">
<v-card-text>
<v-textarea required hide-details name="content" variant="outlined" label="What's happened?!" />
<div class="flex mt-1">
<v-tooltip text="Planned publish" location="start">
<template #activator="{ props }">
<v-btn
v-bind="props"
type="button"
variant="text"
icon="mdi-calendar"
size="small"
@click="dialogs.plan = true"
/>
</template>
</v-tooltip>
<v-tooltip text="Categories" location="start">
<template #activator="{ props }">
<v-btn
v-bind="props"
type="button"
variant="text"
icon="mdi-shape"
size="small"
@click="dialogs.categories = true"
/>
</template>
</v-tooltip>
<v-tooltip text="Media" location="start">
<template #activator="{ props }">
<v-btn
v-bind="props"
type="button"
variant="text"
icon="mdi-camera"
size="small"
@click="dialogs.media = true"
/>
</template>
</v-tooltip>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="editor.show.moment = false">Cancel</v-btn>
<v-btn type="submit" :disabled="loading">Publish</v-btn>
</v-card-actions>
</v-form>
</v-card>
<v-dialog eager v-model="dialogs.plan" class="max-w-[540px]">
<v-card title="Plan your publish">
<template #text>
<v-text-field
v-model="extras.publishedAt"
class="mt-2"
label="Publish date"
hint="Your post will hidden for public before this time. Leave blank will publish immediately"
variant="outlined"
type="datetime-local"
clearable
/>
</template>
<template #actions>
<v-btn class="ms-auto" text="Ok" @click="dialogs.plan = false"></v-btn>
</template>
</v-card>
</v-dialog>
<v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { reactive, ref } from "vue"
const editor = useEditor()
const dialogs = reactive({
plan: false,
categories: false,
media: false
})
const extras = reactive({
publishedAt: null
})
const error = ref<string | null>(null)
const success = ref(false)
const loading = ref(false)
async function postMoment(evt: SubmitEvent) {
const data = new FormData(evt.target as HTMLFormElement)
if (!data.has("content")) return
if (!extras.publishedAt) data.set("published_at", new Date().toISOString())
else data.set("published_at", extras.publishedAt)
loading.value = true
const res = await request("/api/p/moments", {
method: "POST",
headers: { Authorization: `Bearer ${getAtk()}` },
body: data
})
if (res.status === 200) {
success.value = true
} else {
error.value = await res.text()
}
loading.value = false
editor.show.moment = false
}
</script>

View File

@ -0,0 +1,16 @@
<template>
<v-dialog v-model="editor.show.moment" class="max-w-[540px]">
<moment-editor />
</v-dialog>
<v-dialog v-model="editor.show.comment" class="max-w-[540px]">
<comment-editor />
</v-dialog>
</template>
<script setup lang="ts">
import { useEditor } from "@/stores/editor"
import MomentEditor from "@/components/publish/MomentEditor.vue"
import CommentEditor from "@/components/publish/CommentEditor.vue";
const editor = useEditor()
</script>

View File

@ -1,47 +0,0 @@
<template>
<v-dialog v-model="editor.show.moment" class="max-w-[540px]">
<v-card title="Record a moment">
<v-form>
<v-card-text>
<v-textarea
required
hide-details
variant="outlined"
label="What's happened?!"
/>
<div class="flex mt-1">
<v-tooltip text="Planned publish" location="start">
<template #activator="{ props }">
<v-btn v-bind="props" type="button" variant="text" icon="mdi-calendar" size="small" />
</template>
</v-tooltip>
<v-tooltip text="Categories" location="start">
<template #activator="{ props }">
<v-btn v-bind="props" type="button" variant="text" icon="mdi-shape" size="small" />
</template>
</v-tooltip>
<v-tooltip text="Media" location="start">
<template #activator="{ props }">
<v-btn v-bind="props" type="button" variant="text" icon="mdi-camera" size="small" />
</template>
</v-tooltip>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="editor.show.moment = false">Cancel</v-btn>
<v-btn type="submit" @click.prevent>Publish</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { useEditor } from "@/stores/editor";
const editor = useEditor();
</script>

View File

@ -1,7 +1,6 @@
<template> <template>
<v-navigation-drawer v-model="drawerOpen" color="grey-lighten-5" floating> <v-navigation-drawer v-model="drawerOpen" color="grey-lighten-5" floating>
<v-list density="compact" nav> <v-list density="compact" nav> </v-list>
</v-list>
</v-navigation-drawer> </v-navigation-drawer>
<v-app-bar height="64" color="primary" scroll-behavior="elevate" flat> <v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
@ -35,14 +34,7 @@
transition="scroll-y-reverse-transition" transition="scroll-y-reverse-transition"
> >
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-fab <v-fab v-bind="props" class="editor-fab" icon="mdi-pencil" color="primary" size="64" appear />
v-bind="props"
class="editor-fab"
icon="mdi-pencil"
color="primary"
size="64"
appear
/>
</template> </template>
<div class="flex flex-col items-center gap-4 mb-4"> <div class="flex flex-col items-center gap-4 mb-4">
@ -51,23 +43,21 @@
</div> </div>
</v-menu> </v-menu>
<post-editor /> <post-action />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue"
import { useEditor } from "@/stores/editor"; import { useEditor } from "@/stores/editor"
import PostEditor from "@/components/publish/PostEditor.vue"; import PostAction from "@/components/publish/PostAction.vue"
const editor = useEditor(); const editor = useEditor()
const navigationMenu = [ const navigationMenu = [{ name: "Explore", icon: "mdi-compass", to: "explore" }]
{ name: "Explore", icon: "mdi-compass", to: "explore" }
];
const drawerOpen = ref(true); const drawerOpen = ref(true)
function toggleDrawer() { function toggleDrawer() {
drawerOpen.value = !drawerOpen.value; drawerOpen.value = !drawerOpen.value
} }
</script> </script>

View File

@ -1,11 +1,20 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia"
import { reactive, ref } from "vue"; import { reactive, ref } from "vue"
export const useEditor = defineStore("editor", () => { export const useEditor = defineStore("editor", () => {
const done = ref(false)
const show = reactive({ const show = reactive({
moment: false, moment: false,
article: false, article: false,
}); comment: false
})
return { show }; const related = reactive<{ comment_to: any; reply_to: any; repost_to: any }>({
}); comment_to: null,
reply_to: null,
repost_to: null
})
return { show, related, done }
})

View File

@ -30,7 +30,12 @@
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px]"> <div class="aside sticky top-0 w-full h-fit md:min-w-[280px]">
<v-card title="Comments"> <v-card title="Comments">
<div class="px-[1rem] pb-[0.825rem] mt-[-12px]"> <div class="px-[1rem] pb-[0.825rem] mt-[-12px]">
<comment-list v-model:comments="comments" :model="route.params.postType" :alias="route.params.alias" /> <comment-list
v-model:comments="comments"
:item="post"
:model="route.params.postType"
:alias="route.params.alias"
/>
</div> </div>
</v-card> </v-card>
</div> </div>