Creator hub

This commit is contained in:
LittleSheep 2024-02-11 13:12:37 +08:00
parent 4dbbb423e7
commit a5d6071bef
39 changed files with 1015 additions and 257 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View 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>

View 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
View 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>

View 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
View 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

Binary file not shown.

View File

@ -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"`

View 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,
})
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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,

View File

@ -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",

View 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>
)
}

View File

@ -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>

View 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>
);
}

View File

@ -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";

View File

@ -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>

View 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>
</>
);
}

View File

@ -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;
}
} }

View File

@ -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>

View File

@ -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();

View File

@ -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" }
]; ];

View File

@ -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

View 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")}
/>
</>
);
}

View 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>
</>
);
}

View 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")}
/>
</>
);
}

View 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;
}
}

View 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>
);
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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";

View File

@ -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() {

View File

@ -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>