✨ 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 {
|
type Post struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
|
// TODO Introduce thumbnail
|
||||||
Alias string `json:"alias" gorm:"uniqueIndex"`
|
Alias string `json:"alias" gorm:"uniqueIndex"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
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{
|
tx := database.C.Where(&models.Post{
|
||||||
Alias: id,
|
Alias: id,
|
||||||
})
|
}).Where("published_at <= ? OR published_at IS NULL", time.Now())
|
||||||
|
|
||||||
post, err := services.GetPost(tx)
|
post, err := services.GetPost(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -40,6 +40,17 @@ func listOwnedRealm(c *fiber.Ctx) error {
|
|||||||
return c.JSON(realms)
|
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 {
|
func createRealm(c *fiber.Ctx) error {
|
||||||
user := c.Locals("principal").(models.Account)
|
user := c.Locals("principal").(models.Account)
|
||||||
if user.PowerLevel < 10 {
|
if user.PowerLevel < 10 {
|
||||||
|
@ -76,8 +76,12 @@ func NewServer() {
|
|||||||
api.Put("/posts/:postId", auth, editPost)
|
api.Put("/posts/:postId", auth, editPost)
|
||||||
api.Delete("/posts/:postId", auth, deletePost)
|
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", listRealm)
|
||||||
api.Get("/realms/me", auth, listOwnedRealm)
|
api.Get("/realms/me", auth, listOwnedRealm)
|
||||||
|
api.Get("/realms/me/available", auth, listAvailableRealm)
|
||||||
api.Get("/realms/:realmId", getRealm)
|
api.Get("/realms/:realmId", getRealm)
|
||||||
api.Post("/realms", auth, createRealm)
|
api.Post("/realms", auth, createRealm)
|
||||||
api.Post("/realms/:realmId/invite", auth, inviteRealm)
|
api.Post("/realms/:realmId/invite", auth, inviteRealm)
|
||||||
|
@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
||||||
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
|
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListRealm() ([]models.Realm, error) {
|
func ListRealm() ([]models.Realm, error) {
|
||||||
@ -23,6 +24,28 @@ func ListRealmWithUser(user models.Account) ([]models.Realm, error) {
|
|||||||
return realms, nil
|
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) {
|
func NewRealm(user models.Account, name, description string, isPublic bool) (models.Realm, error) {
|
||||||
realm := models.Realm{
|
realm := models.Realm{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
"@solidjs/router": "^0.10.10",
|
"@solidjs/router": "^0.10.10",
|
||||||
"artplayer": "^5.1.1",
|
"artplayer": "^5.1.1",
|
||||||
|
"cherry-markdown": "^0.8.38",
|
||||||
"dompurify": "^3.0.8",
|
"dompurify": "^3.0.8",
|
||||||
"flv.js": "^1.6.2",
|
"flv.js": "^1.6.2",
|
||||||
"hls.js": "^1.5.3",
|
"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 { closeModel, openModel } from "../../scripts/modals.ts";
|
||||||
import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||||
|
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||||
|
|
||||||
import styles from "./PostPublish.module.css";
|
import styles from "./PostPublish.module.css";
|
||||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
|
||||||
|
|
||||||
export default function PostPublish(props: {
|
export default function PostEditActions(props: {
|
||||||
replying?: any,
|
|
||||||
reposting?: any,
|
|
||||||
editing?: any,
|
editing?: any,
|
||||||
realmId?: number,
|
onInputAlias: (value: string) => void,
|
||||||
onReset: () => void,
|
onInputPublish: (value: string) => void,
|
||||||
|
onInputAttachments: (value: any[]) => void,
|
||||||
|
onInputCategories: (categories: any[]) => void,
|
||||||
|
onInputTags: (tags: any[]) => void,
|
||||||
onError: (message: string | null) => 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 [uploading, setUploading] = createSignal(false);
|
||||||
|
|
||||||
const [attachments, setAttachments] = createSignal<any[]>([]);
|
const [attachments, setAttachments] = createSignal<any[]>(props.editing?.attachments ?? []);
|
||||||
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]);
|
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>(props.editing?.categories ?? []);
|
||||||
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]);
|
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>(props.editing?.tags ?? []);
|
||||||
|
|
||||||
const [attachmentMode, setAttachmentMode] = createSignal(0);
|
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) {
|
async function uploadAttachment(evt: SubmitEvent) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
@ -148,6 +57,7 @@ export default function PostPublish(props: {
|
|||||||
...data,
|
...data,
|
||||||
author_id: userinfo?.profiles?.id
|
author_id: userinfo?.profiles?.id
|
||||||
}]));
|
}]));
|
||||||
|
props.onInputCategories(categories())
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +70,7 @@ export default function PostPublish(props: {
|
|||||||
if (!data.name) return;
|
if (!data.name) return;
|
||||||
|
|
||||||
setCategories(categories().concat([data as any]));
|
setCategories(categories().concat([data as any]));
|
||||||
|
props.onInputCategories(categories())
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,156 +87,83 @@ export default function PostPublish(props: {
|
|||||||
if (!data.name) return;
|
if (!data.name) return;
|
||||||
|
|
||||||
setTags(tags().concat([data as any]));
|
setTags(tags().concat([data as any]));
|
||||||
|
props.onInputTags(tags())
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTag(target: any) {
|
function removeTag(target: any) {
|
||||||
setTags(tags().filter(item => item.alias !== target.alias));
|
setTags(tags().filter(item => item.alias !== target.alias));
|
||||||
}
|
props.onInputTags(tags())
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
setAttachments([]);
|
|
||||||
setCategories([]);
|
|
||||||
setTags([]);
|
|
||||||
props.onReset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}>
|
<div class="flex pl-[20px]">
|
||||||
<div id="publish-identity" class="flex border-y border-base-200">
|
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#alias")}>
|
||||||
<div class="avatar pl-[20px]">
|
<i class="fa-solid fa-link"></i>
|
||||||
<div class="w-12">
|
</button>
|
||||||
<Show when={userinfo?.profiles?.avatar}
|
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}>
|
||||||
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
|
<i class="fa-solid fa-paperclip"></i>
|
||||||
<img alt="avatar" src={userinfo?.profiles?.avatar} />
|
</button>
|
||||||
</Show>
|
<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>
|
<input
|
||||||
<div class="flex flex-grow">
|
name="alias" type="text" placeholder="Type here"
|
||||||
<input name="title" value={props.editing?.title ?? ""}
|
class="input input-bordered w-full"
|
||||||
class={`${styles.publishInput} input w-full`}
|
value={props.editing?.alias ?? ""}
|
||||||
placeholder="The describe for a long content" />
|
onInput={(evt) => props.onInputAlias(evt.target.value)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
<div class="label">
|
||||||
|
|
||||||
<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">
|
|
||||||
<span class="label-text-alt">
|
<span class="label-text-alt">
|
||||||
Leave blank to generate a random string.
|
Leave blank to generate a random string.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<div class="modal-action">
|
|
||||||
<button type="button" class="btn" onClick={() => closeModel("#alias")}>Close</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn" onClick={() => closeModel("#alias")}>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog id="planning-publish" class="modal">
|
<dialog id="planning-publish" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3 class="font-bold text-lg mx-1">Planning Publish</h3>
|
<h3 class="font-bold text-lg mx-1">Planning Publish</h3>
|
||||||
<label class="form-control w-full mt-3">
|
<label class="form-control w-full mt-3">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Published At</span>
|
<span class="label-text">Published At</span>
|
||||||
</div>
|
</div>
|
||||||
<input name="published_at" type="datetime-local" placeholder="Pick a date"
|
<input
|
||||||
class="input input-bordered w-full" />
|
name="published_at" type="datetime-local"
|
||||||
<div class="label">
|
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">
|
<span class="label-text-alt">
|
||||||
Before this time, your post will not be visible for everyone.
|
Before this time, your post will not be visible for everyone.
|
||||||
You can modify this plan on Creator Hub.
|
You can modify this plan on Creator Hub.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<div class="modal-action">
|
|
||||||
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</div>
|
||||||
</form>
|
</dialog>
|
||||||
|
|
||||||
<dialog id="attachments" class="modal">
|
<dialog id="attachments" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
@ -346,7 +184,7 @@ export default function PostPublish(props: {
|
|||||||
<span class="label-text">Pick a file</span>
|
<span class="label-text">Pick a file</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="join">
|
<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" />
|
class="join-item file-input file-input-bordered w-full" />
|
||||||
<button type="submit" class="join-item btn btn-primary" disabled={uploading()}>
|
<button type="submit" class="join-item btn btn-primary" disabled={uploading()}>
|
||||||
<i class="fa-solid fa-upload"></i>
|
<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 { 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 PostAttachments from "./PostAttachments.tsx";
|
||||||
import * as marked from "marked";
|
import * as marked from "marked";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
@ -2,7 +2,8 @@ import { createMemo, createSignal, For, Show } from "solid-js";
|
|||||||
|
|
||||||
import styles from "./PostList.module.css";
|
import styles from "./PostList.module.css";
|
||||||
import PostItem from "./PostItem.tsx";
|
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: {
|
export default function PostList(props: {
|
||||||
noRelated?: boolean,
|
noRelated?: boolean,
|
||||||
@ -86,10 +87,7 @@ export default function PostList(props: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={loading()}>
|
<Show when={loading()}>
|
||||||
<div class="w-full border-b border-base-200 pt-5 pb-7 text-center">
|
<LoadingAnimation />
|
||||||
<p class="loading loading-lg loading-infinity"></p>
|
|
||||||
<p>Creating fake news...</p>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</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 {
|
.scrollbar-hidden::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
width: 0;
|
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 "@fortawesome/fontawesome-free/css/all.css";
|
||||||
|
|
||||||
import RootLayout from "./layouts/RootLayout.tsx";
|
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 Global from "./pages/global.tsx";
|
||||||
import PostReference from "./pages/post.tsx";
|
import PostReference from "./pages/post.tsx";
|
||||||
|
import CreatorView from "./pages/creators/view.tsx";
|
||||||
import { UserinfoProvider } from "./stores/userinfo.tsx";
|
import { UserinfoProvider } from "./stores/userinfo.tsx";
|
||||||
import { WellKnownProvider } from "./stores/wellKnown.tsx";
|
import { WellKnownProvider } from "./stores/wellKnown.tsx";
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ render(() => (
|
|||||||
<WellKnownProvider>
|
<WellKnownProvider>
|
||||||
<UserinfoProvider>
|
<UserinfoProvider>
|
||||||
<Router root={RootLayout}>
|
<Router root={RootLayout}>
|
||||||
<Route path="/" component={Feed}>
|
<Route path="/" component={FeedView}>
|
||||||
<Route path="/" component={Global} />
|
<Route path="/" component={Global} />
|
||||||
<Route path="/posts/:postId" component={PostReference} />
|
<Route path="/posts/:postId" component={PostReference} />
|
||||||
<Route path="/search" component={lazy(() => import("./pages/search.tsx"))} />
|
<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="/realms/:realmId" component={lazy(() => import("./pages/realms/realm.tsx"))} />
|
||||||
<Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} />
|
<Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} />
|
||||||
</Route>
|
</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" component={lazy(() => import("./pages/auth/callout.tsx"))} />
|
||||||
<Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
|
<Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
|
||||||
</Router>
|
</Router>
|
||||||
|
@ -22,7 +22,7 @@ export default function RootLayout(props: any) {
|
|||||||
}, [ready, userinfo]);
|
}, [ready, userinfo]);
|
||||||
|
|
||||||
function keepGate(path: string, embedded: boolean, e?: BeforeLeaveEventArgs) {
|
function keepGate(path: string, embedded: boolean, e?: BeforeLeaveEventArgs) {
|
||||||
const blacklist = ["/creator"];
|
const blacklist = ["/creators"];
|
||||||
|
|
||||||
if (!userinfo?.isLoggedIn && blacklist.includes(path)) {
|
if (!userinfo?.isLoggedIn && blacklist.includes(path)) {
|
||||||
if (!e?.defaultPrevented) e?.preventDefault();
|
if (!e?.defaultPrevented) e?.preventDefault();
|
||||||
|
@ -11,6 +11,7 @@ interface MenuItem {
|
|||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const nav: MenuItem[] = [
|
const nav: MenuItem[] = [
|
||||||
|
{ label: "Creators", href: "/creators" },
|
||||||
{ label: "Feed", href: "/" },
|
{ label: "Feed", href: "/" },
|
||||||
{ label: "Realms", href: "/realms" }
|
{ label: "Realms", href: "/realms" }
|
||||||
];
|
];
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { createSignal, Show } from "solid-js";
|
import { createSignal, Show } from "solid-js";
|
||||||
import { useParams } from "@solidjs/router";
|
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 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 { createStore } from "solid-js/store";
|
||||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||||
|
|
||||||
@ -66,7 +66,6 @@ export default function AccountPage() {
|
|||||||
|
|
||||||
<NameCard accountId={params["accountId"]} onError={setError} />
|
<NameCard accountId={params["accountId"]} onError={setError} />
|
||||||
|
|
||||||
|
|
||||||
<dialog id="post-publish" class="modal">
|
<dialog id="post-publish" class="modal">
|
||||||
<div class="modal-box p-0 w-[540px]">
|
<div class="modal-box p-0 w-[540px]">
|
||||||
<PostPublish
|
<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 { createSignal, Show } from "solid-js";
|
||||||
|
|
||||||
import PostList from "../components/PostList.tsx";
|
import PostList from "../components/posts/PostList.tsx";
|
||||||
import PostPublish from "../components/PostPublish.tsx";
|
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
@ -2,9 +2,9 @@ import { createSignal, Show } from "solid-js";
|
|||||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||||
import PostPublish from "../components/PostPublish.tsx";
|
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||||
import PostList from "../components/PostList.tsx";
|
import PostList from "../components/posts/PostList.tsx";
|
||||||
import PostItem from "../components/PostItem.tsx";
|
import PostItem from "../components/posts/PostItem.tsx";
|
||||||
import { getAtk } from "../stores/userinfo.tsx";
|
import { getAtk } from "../stores/userinfo.tsx";
|
||||||
|
|
||||||
export default function PostPage() {
|
export default function PostPage() {
|
||||||
|
@ -2,8 +2,8 @@ import { createSignal, Show } from "solid-js";
|
|||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { useNavigate, useParams } from "@solidjs/router";
|
import { useNavigate, useParams } from "@solidjs/router";
|
||||||
|
|
||||||
import PostList from "../../components/PostList.tsx";
|
import PostList from "../../components/posts/PostList.tsx";
|
||||||
import PostPublish from "../../components/PostPublish.tsx";
|
import PostPublish from "../../components/posts/PostPublish.tsx";
|
||||||
|
|
||||||
import styles from "./realm.module.css";
|
import styles from "./realm.module.css";
|
||||||
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useNavigate, useSearchParams } from "@solidjs/router";
|
import { useNavigate, useSearchParams } from "@solidjs/router";
|
||||||
import { createSignal, Show } from "solid-js";
|
import { createSignal, Show } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import PostPublish from "../components/PostPublish.tsx";
|
import PostPublish from "../components/posts/PostPublish.tsx";
|
||||||
import PostList from "../components/PostList.tsx";
|
import PostList from "../components/posts/PostList.tsx";
|
||||||
import { closeModel, openModel } from "../scripts/modals.ts";
|
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import styles from "./view.module.css";
|
import styles from "./view.module.css";
|
||||||
|
|
||||||
export default function DashboardPage(props: any) {
|
export default function FeedView(props: any) {
|
||||||
return (
|
return (
|
||||||
<div class={`${styles.wrapper} container mx-auto`}>
|
<div class={`${styles.wrapper} container mx-auto`}>
|
||||||
<div id="trending" class="card shadow-xl h-fit"></div>
|
<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}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user