Compare commits
	
		
			3 Commits
		
	
	
		
			7d3236550c
			...
			db5d631049
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| db5d631049 | |||
| 2d7dd26882 | |||
| b0834f48d4 | 
| @@ -18,6 +18,9 @@ | |||||||
|     "@fingerprintjs/fingerprintjs": "^4.6.2", |     "@fingerprintjs/fingerprintjs": "^4.6.2", | ||||||
|     "@fontsource-variable/nunito": "^5.2.6", |     "@fontsource-variable/nunito": "^5.2.6", | ||||||
|     "@hcaptcha/vue3-hcaptcha": "^1.3.0", |     "@hcaptcha/vue3-hcaptcha": "^1.3.0", | ||||||
|  |     "@milkdown/crepe": "^7.15.2", | ||||||
|  |     "@milkdown/kit": "^7.15.2", | ||||||
|  |     "@milkdown/vue": "^7.15.2", | ||||||
|     "@tailwindcss/typography": "^0.5.16", |     "@tailwindcss/typography": "^0.5.16", | ||||||
|     "@tailwindcss/vite": "^4.1.11", |     "@tailwindcss/vite": "^4.1.11", | ||||||
|     "@vueuse/core": "^13.5.0", |     "@vueuse/core": "^13.5.0", | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ | |||||||
|       <img src="/image-broken.jpg" class="w-32 h-32 rounded-md" /> |       <img src="/image-broken.jpg" class="w-32 h-32 rounded-md" /> | ||||||
|     </template> |     </template> | ||||||
|   </n-image> |   </n-image> | ||||||
|  |   <audio v-else-if="itemType == 'audio'" :src="remoteSource" controls /> | ||||||
|  |   <video v-else-if="itemType == 'video'" :src="remoteSource" controls /> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| @@ -14,5 +16,5 @@ const props = defineProps<{ item: any }>() | |||||||
|  |  | ||||||
| const itemType = computed(() => props.item.mime_type.split('/')[0] ?? 'unknown') | const itemType = computed(() => props.item.mime_type.split('/')[0] ?? 'unknown') | ||||||
|  |  | ||||||
| const remoteSource = computed(() => `/cgi/drive/files/${props.item.id}`) | const remoteSource = computed(() => `/cgi/drive/files/${props.item.id}?original=true`) | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										178
									
								
								DysonNetwork.Sphere/Client/src/components/PostEditor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								DysonNetwork.Sphere/Client/src/components/PostEditor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | |||||||
|  | <template> | ||||||
|  |   <n-upload | ||||||
|  |     abstract | ||||||
|  |     with-credentials | ||||||
|  |     @remove="handleRemove" | ||||||
|  |     :create-thumbnail-url="createThumbnailUrl" | ||||||
|  |     :custom-request="customRequest" | ||||||
|  |     v-model:file-list="fileList" | ||||||
|  |   > | ||||||
|  |     <div class="flex flex-col gap-2"> | ||||||
|  |       <pub-select v-model:value="publisher" /> | ||||||
|  |       <n-input | ||||||
|  |         type="textarea" | ||||||
|  |         placeholder="What's happended?!" | ||||||
|  |         v-model:value="content" | ||||||
|  |         @keydown.meta.enter.exact="submit" | ||||||
|  |         @keydown.ctrl.enter.exact="submit" | ||||||
|  |       /> | ||||||
|  |       <n-upload-file-list v-if="fileList" /> | ||||||
|  |       <div class="flex justify-between"> | ||||||
|  |         <div class="flex gap-2"> | ||||||
|  |           <n-upload-trigger #="{ handleClick }" abstract> | ||||||
|  |             <n-button @click="handleClick"> | ||||||
|  |               <n-icon><upload-round /></n-icon> | ||||||
|  |             </n-button> | ||||||
|  |           </n-upload-trigger> | ||||||
|  |         </div> | ||||||
|  |         <n-button type="primary" icon-placement="right" :loading="submitting" @click="submit"> | ||||||
|  |           Post | ||||||
|  |           <template #icon> | ||||||
|  |             <n-icon><send-round /></n-icon> | ||||||
|  |           </template> | ||||||
|  |         </n-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </n-upload> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { | ||||||
|  |   NInput, | ||||||
|  |   NButton, | ||||||
|  |   NIcon, | ||||||
|  |   NUpload, | ||||||
|  |   NUploadFileList, | ||||||
|  |   NUploadTrigger, | ||||||
|  |   useMessage, | ||||||
|  |   type UploadSettledFileInfo, | ||||||
|  |   type UploadCustomRequestOptions, | ||||||
|  |   create, | ||||||
|  |   type UploadFileInfo, | ||||||
|  | } from 'naive-ui' | ||||||
|  | import { SendRound, UploadRound } from '@vicons/material' | ||||||
|  | import { ref } from 'vue' | ||||||
|  | import * as tus from 'tus-js-client' | ||||||
|  |  | ||||||
|  | import PubSelect from './PubSelect.vue' | ||||||
|  |  | ||||||
|  | const emits = defineEmits(['posted']) | ||||||
|  |  | ||||||
|  | const publisher = ref<string | undefined>() | ||||||
|  | const content = ref('') | ||||||
|  |  | ||||||
|  | const fileList = ref<UploadFileInfo[]>([]) | ||||||
|  |  | ||||||
|  | const submitting = ref(false) | ||||||
|  |  | ||||||
|  | async function submit() { | ||||||
|  |   submitting.value = true | ||||||
|  |   await fetch(`/api/posts?pub=${publisher.value}`, { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { | ||||||
|  |       'content-type': 'application/json', | ||||||
|  |     }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       content: content.value, | ||||||
|  |       attachments: fileList.value | ||||||
|  |         .filter((e) => e.url != null) | ||||||
|  |         .map((e) => e.url!.split('/').reverse()[0]), | ||||||
|  |     }), | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   submitting.value = false | ||||||
|  |   content.value = '' | ||||||
|  |   fileList.value = [] | ||||||
|  |   emits('posted') | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const messageDisplay = useMessage() | ||||||
|  |  | ||||||
|  | function customRequest({ | ||||||
|  |   file, | ||||||
|  |   headers, | ||||||
|  |   withCredentials, | ||||||
|  |   onFinish, | ||||||
|  |   onError, | ||||||
|  |   onProgress, | ||||||
|  | }: UploadCustomRequestOptions) { | ||||||
|  |   const requestHeaders: Record<string, string> = {} | ||||||
|  |   const upload = new tus.Upload(file.file, { | ||||||
|  |     endpoint: '/cgi/drive/tus', | ||||||
|  |     retryDelays: [0, 3000, 5000, 10000, 20000], | ||||||
|  |     removeFingerprintOnSuccess: false, | ||||||
|  |     uploadDataDuringCreation: false, | ||||||
|  |     metadata: { | ||||||
|  |       filename: file.name, | ||||||
|  |       'content-type': file.type ?? 'application/octet-stream', | ||||||
|  |     }, | ||||||
|  |     headers: { | ||||||
|  |       'X-DirectUpload': 'true', | ||||||
|  |       ...requestHeaders, | ||||||
|  |       ...headers, | ||||||
|  |     }, | ||||||
|  |     onShouldRetry: () => false, | ||||||
|  |     onError: function (error) { | ||||||
|  |       if (error instanceof tus.DetailedError) { | ||||||
|  |         const failedBody = error.originalResponse?.getBody() | ||||||
|  |         if (failedBody != null) | ||||||
|  |           messageDisplay.error(`Upload failed: ${failedBody}`, { | ||||||
|  |             duration: 10000, | ||||||
|  |             closable: true, | ||||||
|  |           }) | ||||||
|  |       } | ||||||
|  |       console.error('[DRIVE] Upload failed:', error) | ||||||
|  |       onError() | ||||||
|  |     }, | ||||||
|  |     onProgress: function (bytesUploaded, bytesTotal) { | ||||||
|  |       onProgress({ percent: (bytesUploaded / bytesTotal) * 100 }) | ||||||
|  |     }, | ||||||
|  |     onSuccess: function (payload) { | ||||||
|  |       const rawInfo = payload.lastResponse.getHeader('x-fileinfo') | ||||||
|  |       const jsonInfo = JSON.parse(rawInfo as string) | ||||||
|  |       console.log('[DRIVE] Upload successful: ', jsonInfo) | ||||||
|  |       file.url = `/cgi/drive/files/${jsonInfo.id}` | ||||||
|  |       file.type = jsonInfo.mime_type | ||||||
|  |       onFinish() | ||||||
|  |     }, | ||||||
|  |     onBeforeRequest: function (req) { | ||||||
|  |       const xhr = req.getUnderlyingObject() | ||||||
|  |       xhr.withCredentials = withCredentials | ||||||
|  |     }, | ||||||
|  |   }) | ||||||
|  |   upload.findPreviousUploads().then(function (previousUploads) { | ||||||
|  |     if (previousUploads.length) { | ||||||
|  |       upload.resumeFromPreviousUpload(previousUploads[0]) | ||||||
|  |     } | ||||||
|  |     upload.start() | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function createThumbnailUrl( | ||||||
|  |   _file: File | null, | ||||||
|  |   fileInfo: UploadSettledFileInfo, | ||||||
|  | ): string | undefined { | ||||||
|  |   if (!fileInfo) return undefined | ||||||
|  |   return fileInfo.url ?? undefined | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function handleRemove(data: { file: UploadFileInfo; fileList: UploadFileInfo[] }) { | ||||||
|  |   if (data.file.url == null) return true | ||||||
|  |   const messageReactive = messageDisplay.loading('Deleting files...', { | ||||||
|  |     duration: 0, | ||||||
|  |   }) | ||||||
|  |   return new Promise((resolve) => { | ||||||
|  |     fetch(`/cgi/drive/files/${data.file.url!.split('/').reverse()[0]}`, { method: 'DELETE' }) | ||||||
|  |       .then(() => { | ||||||
|  |         messageReactive.destroy() | ||||||
|  |         messageDisplay.success('File has been deleted') | ||||||
|  |         resolve(true) | ||||||
|  |       }) | ||||||
|  |       .catch((err) => { | ||||||
|  |         messageReactive.destroy() | ||||||
|  |         messageDisplay.error('Unable to delete this file: ' + err) | ||||||
|  |         resolve(false) | ||||||
|  |       }) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										19
									
								
								DysonNetwork.Sphere/Client/src/components/PostEditorPro.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								DysonNetwork.Sphere/Client/src/components/PostEditorPro.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <milkdown /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { Milkdown, useEditor } from "@milkdown/vue"; | ||||||
|  | import { Crepe } from "@milkdown/crepe"; | ||||||
|  | import "@milkdown/crepe/theme/common/style.css"; | ||||||
|  | import "@milkdown/crepe/theme/frame.css"; | ||||||
|  |  | ||||||
|  | useEditor((root) => { | ||||||
|  |   const crepe = new Crepe({ | ||||||
|  |     root, | ||||||
|  |   }); | ||||||
|  |   return crepe; | ||||||
|  | }) | ||||||
|  | </script> | ||||||
							
								
								
									
										97
									
								
								DysonNetwork.Sphere/Client/src/components/PubSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								DysonNetwork.Sphere/Client/src/components/PubSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | <template> | ||||||
|  |   <n-select | ||||||
|  |     :options="pubStore.publishers" | ||||||
|  |     label-field="nick" | ||||||
|  |     value-field="name" | ||||||
|  |     :value="props.value" | ||||||
|  |     :render-label="renderLabel" | ||||||
|  |     :render-tag="renderSingleSelectTag" | ||||||
|  |     @update:value="(v) => emits('update:value', v)" | ||||||
|  |   /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { usePubStore } from '@/stores/pub' | ||||||
|  | import { NAvatar, NSelect, NText, type SelectRenderLabel, type SelectRenderTag } from 'naive-ui' | ||||||
|  | import { h, watch } from 'vue' | ||||||
|  |  | ||||||
|  | const pubStore = usePubStore() | ||||||
|  |  | ||||||
|  | const props = defineProps<{ value: string | undefined }>() | ||||||
|  | const emits = defineEmits(['update:value']) | ||||||
|  |  | ||||||
|  | watch( | ||||||
|  |   pubStore, | ||||||
|  |   (value) => { | ||||||
|  |     if (!props.value && value.publishers) { | ||||||
|  |       emits('update:value', pubStore.publishers[0].name) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { deep: true, immediate: true }, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const renderSingleSelectTag: SelectRenderTag = ({ option }: { option: any }) => { | ||||||
|  |   return h( | ||||||
|  |     'div', | ||||||
|  |     { | ||||||
|  |       style: { | ||||||
|  |         display: 'flex', | ||||||
|  |         alignItems: 'center', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     [ | ||||||
|  |       h(NAvatar, { | ||||||
|  |         src: option.picture | ||||||
|  |           ? `/cgi/drive/files/${option.picture.id}` | ||||||
|  |           : undefined, | ||||||
|  |         round: true, | ||||||
|  |         size: 24, | ||||||
|  |         style: { | ||||||
|  |           marginRight: '8px', | ||||||
|  |         }, | ||||||
|  |       }), | ||||||
|  |       option.nick as string, | ||||||
|  |     ], | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const renderLabel: SelectRenderLabel = (option: any) => { | ||||||
|  |   return h( | ||||||
|  |     'div', | ||||||
|  |     { | ||||||
|  |       style: { | ||||||
|  |         display: 'flex', | ||||||
|  |         alignItems: 'center', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     [ | ||||||
|  |       h(NAvatar, { | ||||||
|  |         src: option.picture | ||||||
|  |           ? `/cgi/drive/files/${option.picture.id}` | ||||||
|  |           : undefined, | ||||||
|  |         round: true, | ||||||
|  |         size: 'small', | ||||||
|  |       }), | ||||||
|  |       h( | ||||||
|  |         'div', | ||||||
|  |         { | ||||||
|  |           style: { | ||||||
|  |             marginLeft: '8px', | ||||||
|  |             padding: '4px 0', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         [ | ||||||
|  |           h('div', null, [option.nick as string]), | ||||||
|  |           h( | ||||||
|  |             NText, | ||||||
|  |             { depth: 3, tag: 'div' }, | ||||||
|  |             { | ||||||
|  |               default: () => `@${option.name}`, | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ], | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @@ -15,6 +15,8 @@ import { usePreferredDark } from '@vueuse/core' | |||||||
| import { useUserStore } from './stores/user' | import { useUserStore } from './stores/user' | ||||||
| import { onMounted } from 'vue' | import { onMounted } from 'vue' | ||||||
| import { useServicesStore } from './stores/services' | import { useServicesStore } from './stores/services' | ||||||
|  | import { MilkdownProvider } from '@milkdown/vue' | ||||||
|  | import { usePubStore } from './stores/pub' | ||||||
|  |  | ||||||
| const themeOverrides = { | const themeOverrides = { | ||||||
|   common: { |   common: { | ||||||
| @@ -30,26 +32,30 @@ const isDark = usePreferredDark() | |||||||
|  |  | ||||||
| const userStore = useUserStore() | const userStore = useUserStore() | ||||||
| const servicesStore = useServicesStore() | const servicesStore = useServicesStore() | ||||||
|  | const pubStore = usePubStore() | ||||||
|  |  | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   userStore.initialize() |   userStore.initialize() | ||||||
|  |  | ||||||
|   userStore.fetchUser() |   userStore.fetchUser() | ||||||
|   servicesStore.fetchServices() |   servicesStore.fetchServices() | ||||||
|  |   pubStore.fetchPublishers() | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme"> |   <milkdown-provider> | ||||||
|     <n-global-style /> |     <n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme"> | ||||||
|     <n-loading-bar-provider> |       <n-global-style /> | ||||||
|       <n-dialog-provider> |       <n-loading-bar-provider> | ||||||
|         <n-message-provider placement="bottom"> |         <n-dialog-provider> | ||||||
|           <layout-default> |           <n-message-provider placement="bottom"> | ||||||
|             <router-view /> |             <layout-default> | ||||||
|           </layout-default> |               <router-view /> | ||||||
|         </n-message-provider> |             </layout-default> | ||||||
|       </n-dialog-provider> |           </n-message-provider> | ||||||
|     </n-loading-bar-provider> |         </n-dialog-provider> | ||||||
|   </n-config-provider> |       </n-loading-bar-provider> | ||||||
|  |     </n-config-provider> | ||||||
|  |   </milkdown-provider> | ||||||
| </template> | </template> | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								DysonNetwork.Sphere/Client/src/stores/pub.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								DysonNetwork.Sphere/Client/src/stores/pub.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import { defineStore } from 'pinia' | ||||||
|  | import { ref } from 'vue' | ||||||
|  |  | ||||||
|  | export const usePubStore = defineStore('pub', () => { | ||||||
|  |   const publishers = ref<any[]>([]) | ||||||
|  |  | ||||||
|  |   async function fetchPublishers() { | ||||||
|  |     const resp = await fetch('/api/publishers') | ||||||
|  |     publishers.value = await resp.json() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { publishers, fetchPublishers } | ||||||
|  | }) | ||||||
| @@ -9,7 +9,7 @@ | |||||||
|         </n-infinite-scroll> |         </n-infinite-scroll> | ||||||
|       </n-gi> |       </n-gi> | ||||||
|       <n-gi span="2" class="max-lg:order-first"> |       <n-gi span="2" class="max-lg:order-first"> | ||||||
|         <n-card class="w-full mt-4" title="About"> |         <n-card class="w-full mt-4" title="About" v-if="!userStore.user"> | ||||||
|           <p>Welcome to the <b>Solar Network</b></p> |           <p>Welcome to the <b>Solar Network</b></p> | ||||||
|           <p>The open social network. Friendly to everyone.</p> |           <p>The open social network. Friendly to everyone.</p> | ||||||
|  |  | ||||||
| @@ -22,16 +22,27 @@ | |||||||
|             </span> |             </span> | ||||||
|           </p> |           </p> | ||||||
|         </n-card> |         </n-card> | ||||||
|  |         <n-card class="mt-4 w-full"> | ||||||
|  |           <post-editor @posted="refreshActivities" /> | ||||||
|  |         </n-card> | ||||||
|  |         <n-alert closable class="mt-4" w-full type="info" title="Looking for Solian?"> | ||||||
|  |           The flutter based web app Solian has been moved to | ||||||
|  |           <n-a href="https://web.solian.app" target="_blank">web.solian.app</n-a> | ||||||
|  |           <n-hr /> | ||||||
|  |           网页版 Solian 已经被移动到 | ||||||
|  |           <n-a href="https://web.solian.app" target="_blank">web.solian.app</n-a> | ||||||
|  |         </n-alert> | ||||||
|       </n-gi> |       </n-gi> | ||||||
|     </n-grid> |     </n-grid> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { NCard, NInfiniteScroll, NGrid, NGi } from 'naive-ui' | import { NCard, NInfiniteScroll, NGrid, NGi, NAlert, NA, NHr } from 'naive-ui' | ||||||
| import { computed, onMounted, ref } from 'vue' | import { computed, onMounted, ref } from 'vue' | ||||||
| import { useUserStore } from '@/stores/user' | import { useUserStore } from '@/stores/user' | ||||||
|  |  | ||||||
|  | import PostEditor from '@/components/PostEditor.vue' | ||||||
| import PostItem from '@/components/PostItem.vue' | import PostItem from '@/components/PostItem.vue' | ||||||
|  |  | ||||||
| const userStore = useUserStore() | const userStore = useUserStore() | ||||||
| @@ -46,22 +57,27 @@ onMounted(() => fetchVersion()) | |||||||
| const loading = ref(false) | const loading = ref(false) | ||||||
|  |  | ||||||
| const activites = ref<any[]>([]) | const activites = ref<any[]>([]) | ||||||
| const activitesLast = computed( | const activitesLast = computed(() => activites.value[Math.max(activites.value.length - 1, 0)]) | ||||||
|   () => | const activitesHasMore = ref(true) | ||||||
|     activites.value.sort( |  | ||||||
|       (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), |  | ||||||
|     )[0], |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| async function fetchActivites() { | async function fetchActivites() { | ||||||
|  |   if (loading.value) return | ||||||
|  |   if (!activitesHasMore.value) return | ||||||
|   loading.value = true |   loading.value = true | ||||||
|   const resp = await fetch( |   const resp = await fetch( | ||||||
|     activitesLast.value == null |     activitesLast.value == null | ||||||
|       ? '/api/activities' |       ? '/api/activities' | ||||||
|       : `/api/activities?cursor=${new Date(activitesLast.value.created_at).toISOString()}`, |       : `/api/activities?cursor=${new Date(activitesLast.value.created_at).toISOString()}`, | ||||||
|   ) |   ) | ||||||
|   activites.value.push(...(await resp.json())) |   const data = await resp.json() | ||||||
|  |   activites.value = [...activites.value, ...data] | ||||||
|  |   activitesHasMore.value = data[0]?.type != 'empty' | ||||||
|   loading.value = false |   loading.value = false | ||||||
| } | } | ||||||
| onMounted(() => fetchActivites()) | onMounted(() => fetchActivites()) | ||||||
|  |  | ||||||
|  | async function refreshActivities() { | ||||||
|  |   activites.value = [] | ||||||
|  |   fetchActivites() | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -19,6 +19,10 @@ export default defineConfig({ | |||||||
|   }, |   }, | ||||||
|   server: { |   server: { | ||||||
|     proxy: { |     proxy: { | ||||||
|  |       '/api/tus': { | ||||||
|  |         target: 'http://localhost:5090', | ||||||
|  |         changeOrigin: true, | ||||||
|  |       }, | ||||||
|       '/api': { |       '/api': { | ||||||
|         target: 'http://localhost:5071', |         target: 'http://localhost:5071', | ||||||
|         changeOrigin: true, |         changeOrigin: true, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user