Invite accounts

This commit is contained in:
LittleSheep 2024-03-23 16:23:21 +08:00
parent 311700db04
commit 327941455e
26 changed files with 326 additions and 143 deletions

View File

@ -2,7 +2,7 @@ name: release-nightly
on:
push:
branches: [ master ]
branches: [master]
jobs:
build-docker:

View File

@ -65,9 +65,10 @@ func NewRealm(user models.Account, name, description string, realmType int) (mod
func ListRealmMember(realmId uint) ([]models.RealmMember, error) {
var members []models.RealmMember
if err := database.C.Where(&models.RealmMember{
RealmID: realmId,
}).Find(&members).Error; err != nil {
if err := database.C.
Where(&models.RealmMember{RealmID: realmId}).
Preload("Account").
Find(&members).Error; err != nil {
return members, err
}

View File

@ -1,19 +1,20 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting"
],
parserOptions: {
ecmaVersion: 'latest'
ecmaVersion: "latest"
},
rules: {
'vue/multi-word-component-names': 'off',
'vue/valid-v-for': 'off'
"vue/multi-word-component-names": "off",
"vue/valid-v-for": "off",
"vue/require-v-for-key": "off"
}
}

View File

@ -13,8 +13,8 @@ TypeScript cannot handle type information for `.vue` imports by default, so we r
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration

View File

@ -1,9 +1,9 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/xml+svg" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<link rel="icon" type="image/xml+svg" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Solarplaza</title>
</head>
<body>

View File

@ -1,12 +1,15 @@
html, body, #app, .v-application {
overflow: auto !important;
font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif;
html,
body,
#app,
.v-application {
overflow: auto !important;
font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif;
}
.no-scrollbar {
scrollbar-width: none;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
width: 0;
width: 0;
}

View File

@ -3,13 +3,13 @@
</template>
<script setup lang="ts">
import dompurify from "dompurify";
import { parse } from "marked";
import dompurify from "dompurify"
import { parse } from "marked"
const props = defineProps<{ item: any }>();
const props = defineProps<{ item: any }>()
function parseContent(src: string): string {
return dompurify().sanitize(parse(src) as string);
return dompurify().sanitize(parse(src) as string)
}
</script>

View File

@ -14,7 +14,7 @@
<script setup lang="ts">
import { useEditor } from "@/stores/editor"
import { useUserinfo } from "@/stores/userinfo";
import { useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
const id = useUserinfo()

View File

@ -5,7 +5,7 @@
<div class="mb-3 px-1">
<v-card>
<template #text>
<post-item brief :item="item" @update:item="val => updateItem(idx, val)" />
<post-item brief :item="item" @update:item="(val) => updateItem(idx, val)" />
</template>
</v-card>
</div>
@ -15,14 +15,14 @@
</template>
<script setup lang="ts">
import PostItem from "@/components/posts/PostItem.vue";
import PostItem from "@/components/posts/PostItem.vue"
const props = defineProps<{ posts: any[], loader: (opts: any) => Promise<any> }>();
const emits = defineEmits(["update:posts"]);
const props = defineProps<{ posts: any[]; loader: (opts: any) => Promise<any> }>()
const emits = defineEmits(["update:posts"])
function updateItem(idx: number, data: any) {
const posts = JSON.parse(JSON.stringify(props.posts));
posts[idx] = data;
emits("update:posts", posts);
const posts = JSON.parse(JSON.stringify(props.posts))
posts[idx] = data
emits("update:posts", posts)
}
</script>

View File

@ -25,18 +25,37 @@
You are editing a post with alias <b class="font-mono">{{ editor.related.edit_to?.alias }}</b>
</v-alert>
<v-textarea required class="mb-3" variant="outlined" label="Content"
hint="The content supports markdown syntax" v-model="data.content" @paste="pasteMedia" />
<v-textarea
required
class="mb-3"
variant="outlined"
label="Content"
hint="The content supports markdown syntax"
v-model="data.content"
@paste="pasteMedia"
/>
<v-expansion-panels>
<v-expansion-panel title="Brief describe">
<template #text>
<div class="mt-1">
<v-text-field required variant="solo-filled" density="comfortable" label="Title" :loading="reverting"
v-model="data.title" />
<v-text-field
required
variant="solo-filled"
density="comfortable"
label="Title"
:loading="reverting"
v-model="data.title"
/>
<v-textarea required auto-grow variant="solo-filled" density="comfortable" label="Description"
v-model="data.description" />
<v-textarea
required
auto-grow
variant="solo-filled"
density="comfortable"
label="Description"
v-model="data.description"
/>
</div>
</template>
</v-expansion-panel>
@ -47,7 +66,8 @@
<div>
<p class="text-xs">Your content will visible for public at</p>
<p class="text-lg font-medium">
{{ data.published_at ? new Date(data.published_at).toLocaleString() : new Date().toLocaleString()
{{
data.published_at ? new Date(data.published_at).toLocaleString() : new Date().toLocaleString()
}}
</p>
</div>
@ -103,12 +123,12 @@
import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { useRealms } from "@/stores/realms";
import { computed, reactive, ref, watch } from "vue";
import { useRealms } from "@/stores/realms"
import { computed, reactive, ref, watch } from "vue"
import { useRoute, useRouter } from "vue-router"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import Media from "@/components/publish/parts/Media.vue"
import PublishArea from "@/components/publish/parts/PublishArea.vue";
import PublishArea from "@/components/publish/parts/PublishArea.vue"
const route = useRoute()
const realms = useRealms()
@ -118,7 +138,7 @@ const dialogs = reactive({
plan: false,
categories: false,
media: false,
area: false,
area: false
})
const data = ref<any>({

View File

@ -6,20 +6,40 @@
You are editing a post with alias <b class="font-mono">{{ editor.related.edit_to?.alias }}</b>
</v-alert>
<v-textarea required persistent-counter variant="outlined" label="What's happened?!" counter="1024"
v-model="data.content" @paste="pasteMedia" />
<v-textarea
required
persistent-counter
variant="outlined"
label="What's happened?!"
counter="1024"
v-model="data.content"
@paste="pasteMedia"
/>
<div class="flex mt-[-18px]">
<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" />
<v-btn
v-bind="props"
type="button"
variant="text"
icon="mdi-calendar"
size="small"
@click="dialogs.plan = true"
/>
</template>
</v-tooltip>
<v-tooltip text="Media" location="start">
<template #activator="{ props }">
<v-btn v-bind="props" icon class="text-none" type="button" variant="text" size="small"
@click="dialogs.media = true">
<v-btn
v-bind="props"
icon
class="text-none"
type="button"
variant="text"
size="small"
@click="dialogs.media = true"
>
<v-badge v-if="data.attachments.length > 0" :content="data.attachments.length">
<v-icon icon="mdi-camera" />
</v-badge>

View File

@ -20,6 +20,7 @@
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { ref } from "vue"
@ -35,7 +36,7 @@ async function deletePost() {
const url = `/api/p/${target.model_type}/${target.id}`
loading.value = true
const res = await fetch(url, {
const res = await request(url, {
method: "DELETE",
headers: { Authorization: `Bearer ${getAtk()}` }
})

View File

@ -28,10 +28,10 @@
</template>
<script setup lang="ts">
import { useRealms } from "@/stores/realms";
import { useRealms } from "@/stores/realms"
const realms = useRealms();
const realms = useRealms()
const props = defineProps<{ show: boolean; value: string | null }>();
const emits = defineEmits(["update:show", "update:value"]);
const props = defineProps<{ show: boolean; value: string | null }>()
const emits = defineEmits(["update:show", "update:value"])
</script>

View File

@ -13,8 +13,8 @@
</template>
<script setup lang="ts">
import { useRealms } from "@/stores/realms";
import { useUserinfo } from "@/stores/userinfo";
import { useRealms } from "@/stores/realms"
import { useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
const id = useUserinfo()

View File

@ -20,6 +20,7 @@
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { useRealms } from "@/stores/realms"
import { getAtk } from "@/stores/userinfo"
import { useRoute, useRouter } from "vue-router"
@ -40,7 +41,7 @@ async function deletePost() {
const url = `/api/realms/${target.id}`
loading.value = true
const res = await fetch(url, {
const res = await request(url, {
method: "DELETE",
headers: { Authorization: `Bearer ${getAtk()}` }
})

View File

@ -4,8 +4,15 @@
<v-card-text>
<v-text-field label="Name" variant="outlined" density="comfortable" v-model="data.name" />
<v-textarea label="Description" variant="outlined" density="comfortable" v-model="data.description" />
<v-select label="Realm type" item-title="label" item-value="value" variant="outlined" density="comfortable"
:items="realmTypeOptions" v-model="data.realm_type" />
<v-select
label="Realm type"
item-title="label"
item-value="value"
variant="outlined"
density="comfortable"
:items="realmTypeOptions"
v-model="data.realm_type"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
@ -24,6 +31,7 @@
import { ref, watch } from "vue"
import { getAtk } from "@/stores/userinfo"
import { useRealms } from "@/stores/realms"
import { request } from "@/scripts/request"
const emits = defineEmits(["relist"])
@ -53,7 +61,7 @@ async function submit(evt: SubmitEvent) {
const method = realms.related.edit_to ? "PUT" : "POST"
loading.value = true
const res = await fetch(url, {
const res = await request(url, {
method: method,
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
body: JSON.stringify(payload)

View File

@ -0,0 +1,126 @@
<template>
<div>
<v-list density="comfortable" lines="one">
<v-list-item v-for="item in members" :title="item.account.nick">
<template #prepend>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card me-2"
size="small"
:image="item?.account.avatar"
/>
</template>
<template #subtitle>@{{ item.account.name }}</template>
</v-list-item>
</v-list>
<div v-if="isOwned">
<v-divider class="mt-2 mb-3 border-opacity-50 mx-[-1rem]" />
<div class="px-3">
<v-dialog class="max-w-[540px]">
<template #activator="{ props }">
<v-btn v-bind="props" block prepend-icon="mdi-account-plus" variant="plain"> Invite someone </v-btn>
</template>
<template #default="{ isActive }">
<v-card prepend-icon="mdi-account-plus" title="Invite someone">
<v-form @submit.prevent="inviteMember">
<v-card-text>
<v-text-field
label="Username"
variant="outlined"
density="comfortable"
hint="Require username not the nickname"
v-model="data.account_name"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="isActive.value = false">Cancel</v-btn>
<v-btn type="submit" :disabled="loading">Invite</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
</v-dialog>
</div>
</div>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
import { request } from "@/scripts/request"
import { getAtk, useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
const id = useUserinfo()
const props = defineProps<{ item: any }>()
const data = ref<any>({
account_name: ""
})
const members = ref<any[]>([])
const isOwned = computed(() => {
return id.userinfo.data?.id === props.item?.account_id
})
const loading = ref(false)
const error = ref<string | null>(null)
watch(
() => props.item,
(val) => {
if (val?.id) {
listMembers(val.id)
}
},
{ deep: true, immediate: true }
)
async function listMembers(id: number) {
loading.value = true
const res = await request(`/api/realms/${id}/members`)
if (res.status !== 200) {
error.value = await res.text()
} else {
error.value = null
members.value = await res.json()
}
loading.value = false
}
async function inviteMember(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement
const payload = data.value
loading.value = true
const res = await request(`/api/realms/${props.item?.id}/invite`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
body: JSON.stringify(payload)
})
if (res.status !== 200) {
error.value = await res.text()
} else {
form.reset()
await listMembers(props.item?.id)
}
loading.value = false
}
</script>
<style>
.rounded-card {
border-radius: 8px;
}
</style>

View File

@ -99,7 +99,7 @@ import { useUserinfo } from "@/stores/userinfo"
import { useWellKnown } from "@/stores/wellKnown"
import PostTools from "@/components/publish/PostTools.vue"
import RealmTools from "@/components/realms/RealmTools.vue"
import RealmList from "@/components/realms/RealmList.vue";
import RealmList from "@/components/realms/RealmList.vue"
const id = useUserinfo()
const editor = useEditor()

View File

@ -1,32 +1,32 @@
import "virtual:uno.css";
import "virtual:uno.css"
import "./assets/utils.css";
import "./assets/utils.css"
import { createApp } from "vue";
import { createPinia } from "pinia";
import { createApp } from "vue"
import { createPinia } from "pinia"
import "vuetify/styles";
import { createVuetify } from "vuetify";
import { md3 } from "vuetify/blueprints";
import * as components from "vuetify/components";
import * as labsComponents from 'vuetify/labs/components'
import * as directives from "vuetify/directives";
import "vuetify/styles"
import { createVuetify } from "vuetify"
import { md3 } from "vuetify/blueprints"
import * as components from "vuetify/components"
import * as labsComponents from "vuetify/labs/components"
import * as directives from "vuetify/directives"
import "@mdi/font/css/materialdesignicons.min.css";
import "@fontsource/roboto/latin.css";
import "@unocss/reset/tailwind.css";
import "@mdi/font/css/materialdesignicons.min.css"
import "@fontsource/roboto/latin.css"
import "@unocss/reset/tailwind.css"
import index from "./index.vue";
import router from "./router";
import index from "./index.vue"
import router from "./router"
const app = createApp(index);
const app = createApp(index)
app.use(
createVuetify({
directives,
components: {
...components,
...labsComponents,
...labsComponents
},
blueprint: md3,
theme: {
@ -46,9 +46,9 @@ app.use(
}
}
})
);
)
app.use(createPinia());
app.use(router);
app.use(createPinia())
app.use(router)
app.mount("#app");
app.mount("#app")

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHistory } from "vue-router";
import MasterLayout from "@/layouts/master.vue";
import { createRouter, createWebHistory } from "vue-router"
import MasterLayout from "@/layouts/master.vue"
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -14,7 +14,6 @@ const router = createRouter({
component: () => import("@/views/explore.vue")
},
{
path: "/p/moments/:alias",
name: "posts.details.moments",
@ -34,6 +33,6 @@ const router = createRouter({
]
}
]
});
})
export default router;
export default router

View File

@ -1,6 +1,7 @@
import { reactive, ref } from "vue"
import { defineStore } from "pinia"
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
import { request } from "@/scripts/request"
export const useRealms = defineStore("realms", () => {
const done = ref(false)
@ -20,7 +21,7 @@ export const useRealms = defineStore("realms", () => {
async function list() {
if (!checkLoggedIn()) return
const res = await fetch("/api/realms/me/available", {
const res = await request("/api/realms/me/available", {
headers: { Authorization: `Bearer ${getAtk()}` }
})
if (res.status !== 200) {

View File

@ -31,25 +31,25 @@ export const useUserinfo = defineStore("userinfo", () => {
async function readProfiles() {
if (!checkLoggedIn()) {
isReady.value = true;
}
isReady.value = true
}
const res = await request("/api/users/me", {
headers: { "Authorization": `Bearer ${getAtk()}` }
});
const res = await request("/api/users/me", {
headers: { Authorization: `Bearer ${getAtk()}` }
})
if (res.status !== 200) {
return;
}
if (res.status !== 200) {
return
}
const data = await res.json();
const data = await res.json()
userinfo.value = {
isReady: true,
isLoggedIn: true,
displayName: data["nick"],
data: data
};
userinfo.value = {
isReady: true,
isLoggedIn: true,
displayName: data["nick"],
data: data
}
}
return { userinfo, isReady, readProfiles }

View File

@ -15,46 +15,49 @@
</template>
<script setup lang="ts">
import PostList from "@/components/posts/PostList.vue";
import { reactive, ref } from "vue";
import { request } from "@/scripts/request";
import PostList from "@/components/posts/PostList.vue"
import { reactive, ref } from "vue"
import { request } from "@/scripts/request"
const error = ref<string | null>(null);
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const error = ref<string | null>(null)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const posts = ref<any[]>([]);
const posts = ref<any[]>([])
async function readPosts() {
const res = await request(`/api/feed?` + new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString()
}));
const res = await request(
`/api/feed?` +
new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString()
})
)
if (res.status !== 200) {
error.value = await res.text();
error.value = await res.text()
} else {
error.value = null;
const data = await res.json();
pagination.total = data["count"];
posts.value.push(...(data["data"] ?? []));
error.value = null
const data = await res.json()
pagination.total = data["count"]
posts.value.push(...(data["data"] ?? []))
}
}
async function readMore({ done }: any) {
// Reach the end of data
if (pagination.total <= pagination.page * pagination.pageSize) {
done("empty");
return;
done("empty")
return
}
pagination.page++;
await readPosts();
pagination.page++
await readPosts()
if (error.value != null) done("error");
if (error.value != null) done("error")
else {
if (pagination.total > 0) done("ok");
else done("empty");
if (pagination.total > 0) done("ok")
else done("empty")
}
}
readPosts();
readPosts()
</script>

View File

@ -5,7 +5,7 @@
</div>
<div class="aside md:sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first">
<v-card title="Realm Info" :loading="loading">
<v-card :loading="loading">
<template #title>
<div class="flex justify-between">
<span>Realm Info</span>
@ -23,6 +23,10 @@
</div>
</template>
</v-card>
<v-card class="mt-3 pb-3" title="Realm Members">
<realm-members :item="metadata" />
</v-card>
</div>
</v-container>
</template>
@ -36,6 +40,7 @@ import { parse } from "marked"
import dompurify from "dompurify"
import PostList from "@/components/posts/PostList.vue"
import RealmAction from "@/components/realms/RealmAction.vue"
import RealmMembers from "@/components/realms/RealmMembers.vue"
const route = useRoute()
const realms = useRealms()

View File

@ -1,12 +1,6 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"noEmit": true,

View File

@ -1,4 +1,4 @@
import { defineConfig, presetAttributify, presetTypography, presetUno } from "unocss";
import { defineConfig, presetAttributify, presetTypography, presetUno } from "unocss"
export default defineConfig({
presets: [presetAttributify(), presetTypography(), presetUno({ preflight: false })]