✨ Creator hub
This commit is contained in:
parent
4dbbb423e7
commit
a5d6071bef
57
.idea/codeStyles/Project.xml
Normal file
57
.idea/codeStyles/Project.xml
Normal file
@ -0,0 +1,57 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
12
.idea/dataSources.xml
Normal file
12
.idea/dataSources.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="hy_interactive@localhost" uuid="2e2101b2-4037-47ee-88ed-456dc2cb4423">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://localhost:5432/hy_interactive</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
11
.idea/inspectionProfiles/Project_Default.xml
Normal file
11
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<Languages>
|
||||
<language minSize="54" name="TypeScript" />
|
||||
</Languages>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
6
.idea/sqldialects.xml
Normal file
6
.idea/sqldialects.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/pkg/server/posts_api.go" dialect="PostgreSQL" />
|
||||
</component>
|
||||
</project>
|
BIN
pkg/.DS_Store
vendored
Normal file
BIN
pkg/.DS_Store
vendored
Normal file
Binary file not shown.
@ -5,6 +5,7 @@ import "time"
|
||||
type Post struct {
|
||||
BaseModel
|
||||
|
||||
// TODO Introduce thumbnail
|
||||
Alias string `json:"alias" gorm:"uniqueIndex"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
|
95
pkg/server/creators_api.go
Normal file
95
pkg/server/creators_api.go
Normal file
@ -0,0 +1,95 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getOwnPost(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
id := c.Params("postId")
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
tx := database.C.Where(&models.Post{
|
||||
Alias: id,
|
||||
AuthorID: user.ID,
|
||||
})
|
||||
|
||||
post, err := services.GetPost(tx)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
tx = database.C.
|
||||
Where(&models.Post{ReplyID: &post.ID}).
|
||||
Where("published_at <= ? OR published_at IS NULL", time.Now()).
|
||||
Order("created_at desc")
|
||||
|
||||
var count int64
|
||||
if err := tx.
|
||||
Model(&models.Post{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
posts, err := services.ListPost(tx, take, offset)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"data": post,
|
||||
"count": count,
|
||||
"related": posts,
|
||||
})
|
||||
}
|
||||
|
||||
func listOwnPost(c *fiber.Ctx) error {
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
realmId := c.QueryInt("realmId", 0)
|
||||
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
tx := database.C.
|
||||
Where(&models.Post{AuthorID: user.ID}).
|
||||
Where("published_at <= ? OR published_at IS NULL", time.Now()).
|
||||
Order("created_at desc")
|
||||
|
||||
if realmId > 0 {
|
||||
tx = tx.Where(&models.Post{RealmID: lo.ToPtr(uint(realmId))})
|
||||
} else {
|
||||
tx = tx.Where("realm_id IS NULL")
|
||||
}
|
||||
|
||||
if len(c.Query("category")) > 0 {
|
||||
tx = services.FilterPostWithCategory(tx, c.Query("category"))
|
||||
}
|
||||
|
||||
if len(c.Query("tag")) > 0 {
|
||||
tx = services.FilterPostWithTag(tx, c.Query("tag"))
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := tx.
|
||||
Model(&models.Post{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
posts, err := services.ListPost(tx, take, offset)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": posts,
|
||||
})
|
||||
}
|
@ -19,7 +19,7 @@ func getPost(c *fiber.Ctx) error {
|
||||
|
||||
tx := database.C.Where(&models.Post{
|
||||
Alias: id,
|
||||
})
|
||||
}).Where("published_at <= ? OR published_at IS NULL", time.Now())
|
||||
|
||||
post, err := services.GetPost(tx)
|
||||
if err != nil {
|
||||
|
@ -40,6 +40,17 @@ func listOwnedRealm(c *fiber.Ctx) error {
|
||||
return c.JSON(realms)
|
||||
}
|
||||
|
||||
func listAvailableRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
realms, err := services.ListRealmIsAvailable(user)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(realms)
|
||||
}
|
||||
|
||||
func createRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
if user.PowerLevel < 10 {
|
||||
|
@ -76,8 +76,12 @@ func NewServer() {
|
||||
api.Put("/posts/:postId", auth, editPost)
|
||||
api.Delete("/posts/:postId", auth, deletePost)
|
||||
|
||||
api.Get("/creators/posts", auth, listOwnPost)
|
||||
api.Get("/creators/posts/:postId", auth, getOwnPost)
|
||||
|
||||
api.Get("/realms", listRealm)
|
||||
api.Get("/realms/me", auth, listOwnedRealm)
|
||||
api.Get("/realms/me/available", auth, listAvailableRealm)
|
||||
api.Get("/realms/:realmId", getRealm)
|
||||
api.Post("/realms", auth, createRealm)
|
||||
api.Post("/realms/:realmId/invite", auth, inviteRealm)
|
||||
|
@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func ListRealm() ([]models.Realm, error) {
|
||||
@ -23,6 +24,28 @@ func ListRealmWithUser(user models.Account) ([]models.Realm, error) {
|
||||
return realms, nil
|
||||
}
|
||||
|
||||
func ListRealmIsAvailable(user models.Account) ([]models.Realm, error) {
|
||||
var realms []models.Realm
|
||||
var members []models.RealmMember
|
||||
if err := database.C.Where(&models.RealmMember{
|
||||
AccountID: user.ID,
|
||||
}).Find(&members).Error; err != nil {
|
||||
return realms, err
|
||||
}
|
||||
|
||||
idx := lo.Map(members, func(item models.RealmMember, index int) uint {
|
||||
return item.RealmID
|
||||
})
|
||||
|
||||
if err := database.C.Where(&models.Realm{
|
||||
IsPublic: true,
|
||||
}).Or("id IN ?", idx).Find(&realms).Error; err != nil {
|
||||
return realms, err
|
||||
}
|
||||
|
||||
return realms, nil
|
||||
}
|
||||
|
||||
func NewRealm(user models.Account, name, description string, isPublic bool) (models.Realm, error) {
|
||||
realm := models.Realm{
|
||||
Name: name,
|
||||
|
@ -12,6 +12,7 @@
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@solidjs/router": "^0.10.10",
|
||||
"artplayer": "^5.1.1",
|
||||
"cherry-markdown": "^0.8.38",
|
||||
"dompurify": "^3.0.8",
|
||||
"flv.js": "^1.6.2",
|
||||
"hls.js": "^1.5.3",
|
||||
|
8
pkg/view/src/components/LoadingAnimation.tsx
Normal file
8
pkg/view/src/components/LoadingAnimation.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export default function LoadingAnimation() {
|
||||
return (
|
||||
<div class="w-full border-b border-base-200 pt-5 pb-7 text-center">
|
||||
<p class="loading loading-lg loading-infinity"></p>
|
||||
<p>Listening to the latest news...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,119 +1,28 @@
|
||||
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
||||
import { closeModel, openModel } from "../../scripts/modals.ts";
|
||||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
|
||||
import styles from "./PostPublish.module.css";
|
||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||
|
||||
export default function PostPublish(props: {
|
||||
replying?: any,
|
||||
reposting?: any,
|
||||
export default function PostEditActions(props: {
|
||||
editing?: any,
|
||||
realmId?: number,
|
||||
onReset: () => void,
|
||||
onInputAlias: (value: string) => void,
|
||||
onInputPublish: (value: string) => void,
|
||||
onInputAttachments: (value: any[]) => void,
|
||||
onInputCategories: (categories: any[]) => void,
|
||||
onInputTags: (tags: any[]) => void,
|
||||
onError: (message: string | null) => void,
|
||||
onPost: () => void
|
||||
}) {
|
||||
const userinfo = useUserinfo();
|
||||
const userinfo = useUserinfo()
|
||||
|
||||
if (!userinfo?.isLoggedIn) {
|
||||
return (
|
||||
<div class="py-9 flex justify-center items-center">
|
||||
<div class="text-center">
|
||||
<h2 class="text-lg font-bold">Login!</h2>
|
||||
<p>Or keep silent.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
const [uploading, setUploading] = createSignal(false);
|
||||
|
||||
const [attachments, setAttachments] = createSignal<any[]>([]);
|
||||
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
const [attachments, setAttachments] = createSignal<any[]>(props.editing?.attachments ?? []);
|
||||
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>(props.editing?.categories ?? []);
|
||||
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>(props.editing?.tags ?? []);
|
||||
|
||||
const [attachmentMode, setAttachmentMode] = createSignal(0);
|
||||
|
||||
createEffect(() => {
|
||||
setAttachments(props.editing?.attachments ?? []);
|
||||
setCategories(props.editing?.categories ?? []);
|
||||
setTags(props.editing?.tags ?? []);
|
||||
}, [props.editing]);
|
||||
|
||||
async function doPost(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.content) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await fetch("/api/posts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: data.publish_in_realm ? props.realmId : undefined,
|
||||
published_at: data.published_at ? new Date(data.published_at as string) : new Date(),
|
||||
repost_to: props.reposting?.id,
|
||||
reply_to: props.replying?.id
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function doEdit(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.content) return;
|
||||
if (uploading()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await fetch(`/api/posts/${props.editing?.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: props.realmId,
|
||||
published_at: data.published_at ? new Date(data.published_at as string) : new Date()
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function uploadAttachment(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
@ -148,6 +57,7 @@ export default function PostPublish(props: {
|
||||
...data,
|
||||
author_id: userinfo?.profiles?.id
|
||||
}]));
|
||||
props.onInputCategories(categories())
|
||||
form.reset();
|
||||
}
|
||||
|
||||
@ -160,6 +70,7 @@ export default function PostPublish(props: {
|
||||
if (!data.name) return;
|
||||
|
||||
setCategories(categories().concat([data as any]));
|
||||
props.onInputCategories(categories())
|
||||
form.reset();
|
||||
}
|
||||
|
||||
@ -176,156 +87,83 @@ export default function PostPublish(props: {
|
||||
if (!data.name) return;
|
||||
|
||||
setTags(tags().concat([data as any]));
|
||||
props.onInputTags(tags())
|
||||
form.reset();
|
||||
}
|
||||
|
||||
function removeTag(target: any) {
|
||||
setTags(tags().filter(item => item.alias !== target.alias));
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setAttachments([]);
|
||||
setCategories([]);
|
||||
setTags([]);
|
||||
props.onReset();
|
||||
props.onInputTags(tags())
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}>
|
||||
<div id="publish-identity" class="flex border-y border-base-200">
|
||||
<div class="avatar pl-[20px]">
|
||||
<div class="w-12">
|
||||
<Show when={userinfo?.profiles?.avatar}
|
||||
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
|
||||
<img alt="avatar" src={userinfo?.profiles?.avatar} />
|
||||
</Show>
|
||||
<div class="flex pl-[20px]">
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#alias")}>
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}>
|
||||
<i class="fa-solid fa-paperclip"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}>
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#categories-and-tags")}>
|
||||
<i class="fa-solid fa-tag"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<dialog id="alias" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Permalink</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Alias</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-grow">
|
||||
<input name="title" value={props.editing?.title ?? ""}
|
||||
class={`${styles.publishInput} input w-full`}
|
||||
placeholder="The describe for a long content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.reposting}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are reposting a post from <b>{props.reposting?.author?.nick}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.replying}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are replying a post from <b>{props.replying?.author?.nick}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.editing}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are editing a post published at{" "}
|
||||
<b>{new Date(props.editing?.created_at).toLocaleString()}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.realmId && !props.editing}>
|
||||
<div class="border-b border-base-200 px-5 h-[48px] flex items-center">
|
||||
<div class="form-control flex-grow">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Publish in this realm</span>
|
||||
<input name="publish_in_realm" type="checkbox" checked class="checkbox checkbox-primary" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<textarea required name="content" value={props.editing?.content ?? ""}
|
||||
class={`${styles.publishInput} textarea w-full`}
|
||||
placeholder="What's happened?! (Support markdown)" />
|
||||
|
||||
<div id="publish-actions" class="flex justify-between border-y border-base-200">
|
||||
<div class="flex pl-[20px]">
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#alias")}>
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}>
|
||||
<i class="fa-solid fa-paperclip"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}>
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#categories-and-tags")}>
|
||||
<i class="fa-solid fa-tag"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="alias" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Permalink</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Alias</span>
|
||||
</div>
|
||||
<input name="alias" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
<input
|
||||
name="alias" type="text" placeholder="Type here"
|
||||
class="input input-bordered w-full"
|
||||
value={props.editing?.alias ?? ""}
|
||||
onInput={(evt) => props.onInputAlias(evt.target.value)}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">
|
||||
Leave blank to generate a random string.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#alias")}>Close</button>
|
||||
</div>
|
||||
</label>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#alias")}>Close</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="planning-publish" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Planning Publish</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Published At</span>
|
||||
</div>
|
||||
<input name="published_at" type="datetime-local" placeholder="Pick a date"
|
||||
class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
<dialog id="planning-publish" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Planning Publish</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Published At</span>
|
||||
</div>
|
||||
<input
|
||||
name="published_at" type="datetime-local"
|
||||
placeholder="Pick a date"
|
||||
class="input input-bordered w-full"
|
||||
value={props.editing?.published_at ?? ""}
|
||||
onInput={(evt) => props.onInputAlias(evt.target.value)}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">
|
||||
Before this time, your post will not be visible for everyone.
|
||||
You can modify this plan on Creator Hub.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button>
|
||||
</div>
|
||||
</label>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="attachments" class="modal">
|
||||
<div class="modal-box">
|
||||
@ -346,7 +184,7 @@ export default function PostPublish(props: {
|
||||
<span class="label-text">Pick a file</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<input required type="file" name="attachment"
|
||||
<input required type="file" name="attachment"
|
||||
class="join-item file-input file-input-bordered w-full" />
|
||||
<button type="submit" class="join-item btn btn-primary" disabled={uploading()}>
|
||||
<i class="fa-solid fa-upload"></i>
|
210
pkg/view/src/components/posts/PostEditor.tsx
Normal file
210
pkg/view/src/components/posts/PostEditor.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js";
|
||||
|
||||
import Cherry from "cherry-markdown";
|
||||
import "cherry-markdown/dist/cherry-markdown.min.css";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
import PostEditActions from "./PostEditActions.tsx";
|
||||
|
||||
export default function PostEditor(props: {
|
||||
editing?: any,
|
||||
onError: (message: string | null) => void,
|
||||
onPost: () => void
|
||||
}) {
|
||||
let editorContainer: any;
|
||||
const [editor, setEditor] = createSignal<Cherry>();
|
||||
const [realmList, setRealmList] = createSignal<any[]>([]);
|
||||
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
|
||||
const [alias, setAlias] = createSignal("");
|
||||
const [publishedAt, setPublishedAt] = createSignal("");
|
||||
const [attachments, setAttachments] = createSignal<any[]>([]);
|
||||
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
|
||||
const theme = createMemo(() => {
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark";
|
||||
} else {
|
||||
return "light";
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
editor()?.setTheme(theme());
|
||||
}, [editor(), theme()]);
|
||||
|
||||
onMount(() => {
|
||||
if (editorContainer) {
|
||||
setEditor(new Cherry({
|
||||
el: editorContainer,
|
||||
value: "Welcome to the creator hub! " +
|
||||
"We provide a better editor than normal mode for you! " +
|
||||
"So you can tell us your mind clearly. " +
|
||||
"Delete this paragraph and getting start!"
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
setAttachments(props.editing?.attachments ?? []);
|
||||
setCategories(props.editing?.categories ?? []);
|
||||
setTags(props.editing?.tags ?? []);
|
||||
}, [props.editing]);
|
||||
|
||||
async function listRealm() {
|
||||
const res = await fetch("/api/realms/me/available", {
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status === 200) {
|
||||
setRealmList(await res.json());
|
||||
}
|
||||
}
|
||||
|
||||
listRealm();
|
||||
|
||||
async function doPost(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!editor()?.getValue()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await fetch("/api/posts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: editor()?.getValue(),
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: parseInt(data.realm as string) !== 0 ? parseInt(data.realm as string) : undefined,
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date()
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function doEdit(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!editor()?.getValue()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await fetch(`/api/posts/${props.editing?.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: editor()?.getValue(),
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date()
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setAttachments([]);
|
||||
setCategories([]);
|
||||
setTags([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onReset={resetForm} onSubmit={(evt) => props.editing ? doEdit(evt) : doPost(evt)}>
|
||||
<div>
|
||||
<div ref={editorContainer}></div>
|
||||
</div>
|
||||
|
||||
<div class="border-y border-base-200">
|
||||
<PostEditActions
|
||||
onInputAlias={setAlias}
|
||||
onInputPublish={setPublishedAt}
|
||||
onInputAttachments={setAttachments}
|
||||
onInputCategories={setCategories}
|
||||
onInputTags={setTags}
|
||||
onError={props.onError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pt-3 pb-7 px-7">
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Publish region</span>
|
||||
</div>
|
||||
<select name="realm" class="select select-bordered" disabled={props.editing}>
|
||||
<option value={0} selected>Global</option>
|
||||
<For each={realmList()}>
|
||||
{item => <option value={item.id}>{item.name}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Will show realms you joined or created.</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Post title</span>
|
||||
</div>
|
||||
<input value={props.editing?.title ?? ""} name="title" type="text" placeholder="Type here"
|
||||
class="input input-bordered w-full" />
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Post description</span>
|
||||
</div>
|
||||
<textarea value={props.editing?.description ?? ""} disabled name="description"
|
||||
placeholder="Not available now"
|
||||
class="textarea textarea-bordered w-full" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Won't display in the post list when your post is too long.</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Post thumbnail</span>
|
||||
</div>
|
||||
<input disabled name="thumbnail" type="file" placeholder="Not available now"
|
||||
class="file-input file-input-bordered w-full" />
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-7" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={"Submit"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
import PostAttachments from "./PostAttachments.tsx";
|
||||
import * as marked from "marked";
|
||||
import DOMPurify from "dompurify";
|
@ -2,7 +2,8 @@ import { createMemo, createSignal, For, Show } from "solid-js";
|
||||
|
||||
import styles from "./PostList.module.css";
|
||||
import PostItem from "./PostItem.tsx";
|
||||
import { getAtk } from "../stores/userinfo.tsx";
|
||||
import LoadingAnimation from "../LoadingAnimation.tsx";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
|
||||
export default function PostList(props: {
|
||||
noRelated?: boolean,
|
||||
@ -86,10 +87,7 @@ export default function PostList(props: {
|
||||
</div>
|
||||
|
||||
<Show when={loading()}>
|
||||
<div class="w-full border-b border-base-200 pt-5 pb-7 text-center">
|
||||
<p class="loading loading-lg loading-infinity"></p>
|
||||
<p>Creating fake news...</p>
|
||||
</div>
|
||||
<LoadingAnimation />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
212
pkg/view/src/components/posts/PostPublish.tsx
Normal file
212
pkg/view/src/components/posts/PostPublish.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
|
||||
import styles from "./PostPublish.module.css";
|
||||
import PostEditActions from "./PostEditActions.tsx";
|
||||
|
||||
export default function PostPublish(props: {
|
||||
replying?: any,
|
||||
reposting?: any,
|
||||
editing?: any,
|
||||
realmId?: number,
|
||||
onReset: () => void,
|
||||
onError: (message: string | null) => void,
|
||||
onPost: () => void
|
||||
}) {
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
if (!userinfo?.isLoggedIn) {
|
||||
return (
|
||||
<div class="py-9 flex justify-center items-center">
|
||||
<div class="text-center">
|
||||
<h2 class="text-lg font-bold">Login!</h2>
|
||||
<p>Or keep silent.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
|
||||
const [alias, setAlias] = createSignal("");
|
||||
const [publishedAt, setPublishedAt] = createSignal("");
|
||||
const [attachments, setAttachments] = createSignal<any[]>([]);
|
||||
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]);
|
||||
|
||||
createEffect(() => {
|
||||
setAttachments(props.editing?.attachments ?? []);
|
||||
setCategories(props.editing?.categories ?? []);
|
||||
setTags(props.editing?.tags ?? []);
|
||||
}, [props.editing]);
|
||||
|
||||
async function doPost(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.content) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await fetch("/api/posts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: data.publish_in_realm ? props.realmId : undefined,
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date(),
|
||||
repost_to: props.reposting?.id,
|
||||
reply_to: props.replying?.id
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function doEdit(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.content) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await fetch(`/api/posts/${props.editing?.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${getAtk()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: props.realmId,
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date(),
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
form.reset();
|
||||
props.onError(null);
|
||||
props.onPost();
|
||||
}
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setAttachments([]);
|
||||
setCategories([]);
|
||||
setTags([]);
|
||||
props.onReset();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}>
|
||||
<div id="publish-identity" class="flex border-y border-base-200">
|
||||
<div class="avatar pl-[20px]">
|
||||
<div class="w-12">
|
||||
<Show when={userinfo?.profiles?.avatar}
|
||||
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
|
||||
<img alt="avatar" src={userinfo?.profiles?.avatar} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-grow">
|
||||
<input name="title" value={props.editing?.title ?? ""}
|
||||
class={`${styles.publishInput} input w-full`}
|
||||
placeholder="The describe for a long content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.reposting}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are reposting a post from <b>{props.reposting?.author?.nick}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.replying}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are replying a post from <b>{props.replying?.author?.nick}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.editing}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are editing a post published at{" "}
|
||||
<b>{new Date(props.editing?.created_at).toLocaleString()}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.realmId && !props.editing}>
|
||||
<div class="border-b border-base-200 px-5 h-[48px] flex items-center">
|
||||
<div class="form-control flex-grow">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Publish in this realm</span>
|
||||
<input name="publish_in_realm" type="checkbox" checked class="checkbox checkbox-primary" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<textarea required name="content" value={props.editing?.content ?? ""}
|
||||
class={`${styles.publishInput} textarea w-full`}
|
||||
placeholder="What's happened?! (Support markdown)" />
|
||||
|
||||
<div id="publish-actions" class="flex justify-between border-y border-base-200">
|
||||
<PostEditActions
|
||||
onInputAlias={setAlias}
|
||||
onInputPublish={setPublishedAt}
|
||||
onInputAttachments={setAttachments}
|
||||
onInputCategories={setCategories}
|
||||
onInputTags={setTags}
|
||||
onError={props.onError}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
@ -22,4 +22,18 @@ html, body {
|
||||
.scrollbar-hidden::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.cherry, .cherry-toolbar, .cherry-editor, .cherry-previewer, .cherry-drag {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.cherry-drag {
|
||||
width: 2px !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.cherry-drag {
|
||||
background: oklch(var(--b2)) !important;
|
||||
}
|
||||
}
|
@ -11,9 +11,10 @@ import { Route, Router } from "@solidjs/router";
|
||||
import "@fortawesome/fontawesome-free/css/all.css";
|
||||
|
||||
import RootLayout from "./layouts/RootLayout.tsx";
|
||||
import Feed from "./pages/view.tsx";
|
||||
import FeedView from "./pages/view.tsx";
|
||||
import Global from "./pages/global.tsx";
|
||||
import PostReference from "./pages/post.tsx";
|
||||
import CreatorView from "./pages/creators/view.tsx";
|
||||
import { UserinfoProvider } from "./stores/userinfo.tsx";
|
||||
import { WellKnownProvider } from "./stores/wellKnown.tsx";
|
||||
|
||||
@ -23,7 +24,7 @@ render(() => (
|
||||
<WellKnownProvider>
|
||||
<UserinfoProvider>
|
||||
<Router root={RootLayout}>
|
||||
<Route path="/" component={Feed}>
|
||||
<Route path="/" component={FeedView}>
|
||||
<Route path="/" component={Global} />
|
||||
<Route path="/posts/:postId" component={PostReference} />
|
||||
<Route path="/search" component={lazy(() => import("./pages/search.tsx"))} />
|
||||
@ -31,6 +32,11 @@ render(() => (
|
||||
<Route path="/realms/:realmId" component={lazy(() => import("./pages/realms/realm.tsx"))} />
|
||||
<Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} />
|
||||
</Route>
|
||||
<Route path="/creators" component={CreatorView}>
|
||||
<Route path="/" component={lazy(() => import("./pages/creators"))} />
|
||||
<Route path="/publish" component={lazy(() => import("./pages/creators/publish.tsx"))} />
|
||||
<Route path="/edit/:postId" component={lazy(() => import("./pages/creators/edit.tsx"))} />
|
||||
</Route>
|
||||
<Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} />
|
||||
<Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
|
||||
</Router>
|
||||
|
@ -22,7 +22,7 @@ export default function RootLayout(props: any) {
|
||||
}, [ready, userinfo]);
|
||||
|
||||
function keepGate(path: string, embedded: boolean, e?: BeforeLeaveEventArgs) {
|
||||
const blacklist = ["/creator"];
|
||||
const blacklist = ["/creators"];
|
||||
|
||||
if (!userinfo?.isLoggedIn && blacklist.includes(path)) {
|
||||
if (!e?.defaultPrevented) e?.preventDefault();
|
||||
|
@ -11,6 +11,7 @@ interface MenuItem {
|
||||
|
||||
export default function Navbar() {
|
||||
const nav: MenuItem[] = [
|
||||
{ label: "Creators", href: "/creators" },
|
||||
{ label: "Feed", href: "/" },
|
||||
{ label: "Realms", href: "/realms" }
|
||||
];
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { useParams } from "@solidjs/router";
|
||||
|
||||
import PostList from "../components/PostList.tsx";
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
import NameCard from "../components/NameCard.tsx";
|
||||
import PostPublish from "../components/PostPublish.tsx";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||
|
||||
@ -66,7 +66,6 @@ export default function AccountPage() {
|
||||
|
||||
<NameCard accountId={params["accountId"]} onError={setError} />
|
||||
|
||||
|
||||
<dialog id="post-publish" class="modal">
|
||||
<div class="modal-box p-0 w-[540px]">
|
||||
<PostPublish
|
||||
|
57
pkg/view/src/pages/creators/edit.tsx
Normal file
57
pkg/view/src/pages/creators/edit.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import PostEdit from "../../components/posts/PostEditor.tsx";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
|
||||
export default function PublishPost() {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const [post, setPost] = createSignal<any>();
|
||||
|
||||
async function readPost() {
|
||||
const res = await fetch(`/api/creators/posts/${params["postId"]}`, {
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status === 200) {
|
||||
setPost(await res.json());
|
||||
} else {
|
||||
setError(await res.text());
|
||||
}
|
||||
}
|
||||
|
||||
readPost();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex pt-1 border-b border-base-200">
|
||||
<a class="btn btn-ghost ml-[20px] w-12 h-12" href="/creators">
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
</a>
|
||||
<div class="px-5 flex items-center">
|
||||
<p>Publish a new post</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<PostEdit
|
||||
editing={post()}
|
||||
onError={setError}
|
||||
onPost={() => navigate("/creators")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
120
pkg/view/src/pages/creators/index.tsx
Normal file
120
pkg/view/src/pages/creators/index.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { createMemo, createSignal, For, Show } from "solid-js";
|
||||
import { getAtk } from "../../stores/userinfo.tsx";
|
||||
import LoadingAnimation from "../../components/LoadingAnimation.tsx";
|
||||
import styles from "../../components/posts/PostList.module.css";
|
||||
|
||||
export default function CreatorHub() {
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
const [posts, setPosts] = createSignal<any[]>([]);
|
||||
const [postCount, setPostCount] = createSignal(0);
|
||||
|
||||
const [page, setPage] = createSignal(1);
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
|
||||
const pageCount = createMemo(() => Math.ceil(postCount() / 10));
|
||||
|
||||
async function readPosts(pn?: number) {
|
||||
if (pn) setPage(pn);
|
||||
setLoading(true);
|
||||
const res = await fetch("/api/creators/posts?" + new URLSearchParams({
|
||||
take: (10).toString(),
|
||||
offset: ((page() - 1) * 10).toString()
|
||||
}), { headers: { "Authorization": `Bearer ${getAtk()}` } });
|
||||
if (res.status !== 200) {
|
||||
setError(await res.text());
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(null);
|
||||
setPosts(data["data"]);
|
||||
setPostCount(data["count"]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
readPosts();
|
||||
|
||||
function changePage(pn: number) {
|
||||
readPosts(pn).then(() => {
|
||||
setTimeout(() => window.scrollTo({ top: 0, behavior: "smooth" }), 16);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 px-7 flex items-center justify-between border-b border-base-200">
|
||||
<h3 class="py-3 font-bold">Your posts</h3>
|
||||
<a class="btn btn-primary" href="/creators/publish">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid justify-items-strench">
|
||||
<For each={posts()}>
|
||||
{item =>
|
||||
<a href={`/creators/edit/${item.alias}`}>
|
||||
<div class="card sm:card-side hover:bg-base-200 transition-colors sm:max-w-none">
|
||||
<div class="card-body">
|
||||
<Show when={item?.title} fallback={
|
||||
<div class="line-clamp-3">
|
||||
{item?.content?.replaceAll("#", "").replaceAll("*", "").trim()}
|
||||
</div>
|
||||
}>
|
||||
<h2 class="text-xl">{item?.title}</h2>
|
||||
<div class="mx-[-2px] mt-[-4px]">
|
||||
{item?.categories?.map((category: any) => (
|
||||
<span class="badge badge-primary">{category.name}</span>
|
||||
))}
|
||||
{item?.tags?.map((tag: any) => (
|
||||
<span class="badge badge-secondary">{tag.name}</span>
|
||||
))}
|
||||
</div>
|
||||
<div class="text-sm opacity-80 line-clamp-3">
|
||||
{item?.content?.substring(0, 160).replaceAll("#", "").replaceAll("*", "").trim() + "……"}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="text-xs opacity-70 flex gap-2">
|
||||
<span>Post #{item?.id}</span>
|
||||
<span>Published at {new Date(item?.published_at).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="join">
|
||||
<button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() <= 1}
|
||||
onClick={() => changePage(page() - 1)}>
|
||||
<i class="fa-solid fa-caret-left"></i>
|
||||
</button>
|
||||
<button class="join-item btn btn-ghost">Page {page()}</button>
|
||||
<button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() >= pageCount()}
|
||||
onClick={() => changePage(page() + 1)}>
|
||||
<i class="fa-solid fa-caret-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={loading()}>
|
||||
<LoadingAnimation />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
40
pkg/view/src/pages/creators/publish.tsx
Normal file
40
pkg/view/src/pages/creators/publish.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import PostEdit from "../../components/posts/PostEditor.tsx";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
|
||||
export default function PublishPost() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex pt-1 border-b border-base-200">
|
||||
<a class="btn btn-ghost ml-[20px] w-12 h-12" href="/creators">
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
</a>
|
||||
<div class="px-5 flex items-center">
|
||||
<p>Publish a new post</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alerts">
|
||||
<Show when={error()}>
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="capitalize">{error()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<PostEdit
|
||||
onError={setError}
|
||||
onPost={() => navigate("/creators")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
13
pkg/view/src/pages/creators/view.module.css
Normal file
13
pkg/view/src/pages/creators/view.module.css
Normal file
@ -0,0 +1,13 @@
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
column-gap: 20px;
|
||||
|
||||
max-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wrapper {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
}
|
16
pkg/view/src/pages/creators/view.tsx
Normal file
16
pkg/view/src/pages/creators/view.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import styles from "./view.module.css";
|
||||
|
||||
export default function CreatorView(props: any) {
|
||||
return (
|
||||
<div class={`${styles.wrapper} container mx-auto`}>
|
||||
<div id="nav" class="card shadow-xl h-fit">
|
||||
<h2 class="text-xl font-bold mt-1 py-5 px-7">Creator Hub</h2>
|
||||
</div>
|
||||
|
||||
<div id="content" class="card shadow-xl">
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
|
||||
import PostList from "../components/PostList.tsx";
|
||||
import PostPublish from "../components/PostPublish.tsx";
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
export default function DashboardPage() {
|
||||
|
@ -2,9 +2,9 @@ import { createSignal, Show } from "solid-js";
|
||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||
import PostPublish from "../components/PostPublish.tsx";
|
||||
import PostList from "../components/PostList.tsx";
|
||||
import PostItem from "../components/PostItem.tsx";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
import PostItem from "../components/posts/PostItem.tsx";
|
||||
import { getAtk } from "../stores/userinfo.tsx";
|
||||
|
||||
export default function PostPage() {
|
||||
|
@ -2,8 +2,8 @@ import { createSignal, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
|
||||
import PostList from "../../components/PostList.tsx";
|
||||
import PostPublish from "../../components/PostPublish.tsx";
|
||||
import PostList from "../../components/posts/PostList.tsx";
|
||||
import PostPublish from "../../components/posts/PostPublish.tsx";
|
||||
|
||||
import styles from "./realm.module.css";
|
||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useNavigate, useSearchParams } from "@solidjs/router";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import PostPublish from "../components/PostPublish.tsx";
|
||||
import PostList from "../components/PostList.tsx";
|
||||
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||
import PostList from "../components/posts/PostList.tsx";
|
||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||
|
||||
export default function SearchPage() {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import styles from "./view.module.css";
|
||||
|
||||
export default function DashboardPage(props: any) {
|
||||
export default function FeedView(props: any) {
|
||||
return (
|
||||
<div class={`${styles.wrapper} container mx-auto`}>
|
||||
<div id="trending" class="card shadow-xl h-fit"></div>
|
||||
|
||||
<div id="content max-w-[100vw]" class="card shadow-xl">
|
||||
<div id="content" class="card shadow-xl">
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user