Skip to content

Commit

Permalink
Merge pull request #206 from openRin/139-feature-request
Browse files Browse the repository at this point in the history
feat: #139 feature request
  • Loading branch information
OXeu authored Jul 9, 2024
2 parents d90b390 + 690a21e commit 547109e
Show file tree
Hide file tree
Showing 15 changed files with 403 additions and 18 deletions.
23 changes: 22 additions & 1 deletion client/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@
"confirm": "Are you sure you want to delete this article?",
"title": "Delete Article"
},
"search": {
"placeholder": "Please enter keywords",
"title": "Search",
"title$keyword": "Search: {{keyword}}"
},
"title": "Articles",
"top": {
"confirm": "Are you sure you want to top this article?",
"success": "Topped successfully",
"title": "Top"
},
"total_short$count_one": "{{count}} articles",
"total_short$count_other": "{{count}} articles",
"total$count_one": "{{count}} articles in total",
"total$count_other": "{{count}} articles in total"
"total$count_other": "{{count}} articles in total",
"untop": {
"confirm": "Are you sure you want to untop this article?",
"success": "Untopped successfully",
"title": "Untop"
}
},
"avatar": {
"url": "Avatar URL"
Expand Down Expand Up @@ -162,8 +177,14 @@
"timeline": "Timeline",
"title": "Title",
"title_empty": "Title cannot be empty",
"top": {
"title": "Top"
},
"unlisted": "Unlisted",
"untitled": "Untitled",
"untop": {
"title": "Untop"
},
"update": {
"success": "Update successful",
"title": "Update"
Expand Down
23 changes: 22 additions & 1 deletion client/public/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@
"confirm": "この記事を削除してもよろしいですか?",
"title": "記事の削除"
},
"search": {
"placeholder": "キーワードを入力してください",
"title": "検索",
"title$keyword": "検索: {{keyword}}"
},
"title": "記事",
"top": {
"confirm": "この記事をトップにしてもよろしいですか?",
"success": "トップに成功しました",
"title": "トップ"
},
"total_short$count_one": "{{count}} 記事",
"total_short$count_other": "{{count}} 記事",
"total$count_one": "合計 {{count}} 記事",
"total$count_other": "合計 {{count}} 記事"
"total$count_other": "合計 {{count}} 記事",
"untop": {
"confirm": "この記事のトップを解除してもよろしいですか?",
"success": "トップ解除に成功しました",
"title": "トップ解除"
}
},
"avatar": {
"url": "アバターURL"
Expand Down Expand Up @@ -162,8 +177,14 @@
"timeline": "タイムライン",
"title": "タイトル",
"title_empty": "タイトルは空にできません",
"top": {
"title": "キャッシュをクリア"
},
"unlisted": "リストされていない",
"untitled": "無題",
"untop": {
"title": "トップ解除"
},
"update": {
"success": "更新に成功しました",
"title": "更新"
Expand Down
23 changes: 22 additions & 1 deletion client/public/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@
"confirm": "确定删除这篇文章吗?",
"title": "删除文章"
},
"search": {
"placeholder": "请输入关键字",
"title": "搜索",
"title$keyword": "搜索:{{keyword}}"
},
"title": "文章",
"top": {
"confirm": "确定置顶这篇文章吗?",
"success": "置顶成功",
"title": "置顶"
},
"total_short$count_one": "{{count}} 篇",
"total_short$count_other": "{{count}} 篇",
"total$count_one": "共有 {{count}} 篇文章",
"total$count_other": "共有 {{count}} 篇文章"
"total$count_other": "共有 {{count}} 篇文章",
"untop": {
"confirm": "确定取消置顶这篇文章吗?",
"success": "取消置顶成功",
"title": "取消置顶"
}
},
"avatar": {
"url": "头像 URL"
Expand Down Expand Up @@ -162,8 +177,14 @@
"timeline": "时间轴",
"title": "标题",
"title_empty": "标题不能为空",
"top": {
"title": "置顶"
},
"unlisted": "未列出",
"untitled": "未列出",
"untop": {
"title": "取消置顶"
},
"update": {
"success": "更新成功",
"title": "更新"
Expand Down
7 changes: 7 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ClientConfigContext, ConfigWrapper, defaultClientConfig } from './state
import { Profile, ProfileContext } from './state/profile'
import { headersWithAuth } from './utils/auth'
import { tryInt } from './utils/int'
import { SearchPage } from './page/search.tsx'

function App() {
const ref = useRef(false)
Expand Down Expand Up @@ -90,6 +91,12 @@ function App() {
}}
</RouteMe>

<RouteMe path="/search/:keyword">
{params => {
return (<SearchPage keyword={params.keyword || ""} />)
}}
</RouteMe>

<RouteMe path="/settings" paddingClassName='mx-4'>
<Settings />
</RouteMe>
Expand Down
4 changes: 4 additions & 0 deletions client/src/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
.bg-active {
@apply active:bg-neutral-200 dark:active:bg-neutral-600;
}

.bg-hover {
@apply hover:bg-neutral-200 dark:hover:bg-neutral-600;
}

.shadow-light {
@apply shadow-neutral-200/30 dark:shadow-black/10;
Expand Down
12 changes: 11 additions & 1 deletion client/src/components/feed_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { Link } from "wouter";
import { useTranslation } from "react-i18next";
import { timeago } from "../utils/timeago";
import { HashTag } from "./hashtag";
export function FeedCard({ id, title, avatar, draft, listed, summary, hashtags, createdAt, updatedAt }: { id: string, avatar?: string, draft?: number, listed?: number, title: string, summary: string, hashtags: { id: number, name: string }[], createdAt: Date, updatedAt: Date }) {
export function FeedCard({ id, title, avatar, draft, listed, top, summary, hashtags, createdAt, updatedAt }:
{
id: string, avatar?: string,
draft?: number, listed?: number, top?: number,
title: string, summary: string,
hashtags: { id: number, name: string }[],
createdAt: Date, updatedAt: Date
}) {
const { t } = useTranslation()
return (
<>
Expand All @@ -26,6 +33,9 @@ export function FeedCard({ id, title, avatar, draft, listed, summary, hashtags,
}
{draft === 1 && <span className="text-gray-400 text-sm">草稿</span>}
{listed === 0 && <span className="text-gray-400 text-sm">未列出</span>}
{top === 1 && <span className="text-theme text-sm">
置顶
</span>}
</div>
<p className="text-pretty overflow-hidden dark:text-neutral-500">
{summary}
Expand Down
62 changes: 62 additions & 0 deletions client/src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import ReactModal from "react-modal";
import Popup from "reactjs-popup";
import { removeCookie } from "typescript-cookie";
import { Link, useLocation } from "wouter";
import { oauth_url } from "../main";
import { Profile, ProfileContext } from "../state/profile";
import { Button } from "./button";
import { Icon } from "./icon";
import { Input } from "./input";
import { Padding } from "./padding";


Expand Down Expand Up @@ -54,6 +57,7 @@ export function Header({ children }: { children?: React.ReactNode }) {
</div>
</div>
<div className="ml-auto hidden opacity-0 md:opacity-100 duration-300 md:flex flex-row items-center space-x-2">
<SearchButton />
<LanguageSwitch />
<UserAvatar profile={profile} />
</div>
Expand Down Expand Up @@ -146,6 +150,7 @@ function Menu() {
>
<div className="flex flex-col bg-w rounded-xl p-2 mt-4 w-[50vw]">
<div className="flex flex-row justify-end space-x-2">
<SearchButton onClose={onClose} />
<LanguageSwitch />
<UserAvatar profile={profile} mobile />
</div>
Expand Down Expand Up @@ -210,4 +215,61 @@ function LanguageSwitch({ className }: { className?: string }) {
</Popup>
</div>
)
}

function SearchButton({ className, onClose }: { className?: string, onClose?: () => void }) {
const { t } = useTranslation()
const [isOpened, setIsOpened] = useState(false);
const [_, setLocation] = useLocation()
const [value, setValue] = useState('')
const label = t('article.search.title')
const onSearch = () => {
const key = `${encodeURIComponent(value)}`
setTimeout(() => {
setIsOpened(false)
onClose?.()
}, 100)
if (value.length != 0)
setLocation(`/search/${key}`)
}
return (<div className={className + " flex flex-row items-center"}>
<button onClick={() => setIsOpened(true)} title={label} aria-label={label}
className="flex rounded-full border dark:border-neutral-600 px-2 bg-w aspect-[1] items-center justify-center t-primary bg-active">
<i className="ri-search-line"></i>
</button>
<ReactModal
isOpen={isOpened}
style={{
content: {
top: "20%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
padding: "0",
border: "none",
borderRadius: "16px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
background: "none",
},
overlay: {
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 1000,
},
}}
onRequestClose={() => setIsOpened(false)}
>
<div className="bg-w w-full flex flex-row items-center justify-between p-4 space-x-4">
<Input value={value} setValue={setValue} placeholder={t('article.search.placeholder')}
autofocus
onSubmit={onSearch} />
<Button title={value.length === 0 ? t("close") : label} onClick={onSearch} />
</div>
</ReactModal>
</div>
)
}
15 changes: 12 additions & 3 deletions client/src/components/input.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
export function Input({ value, setValue, className, placeholder }: { value: string, className?: string, placeholder: string, id?: number, setValue: (v: string) => void }) {

export function Input({ autofocus, value, setValue, className, placeholder, onSubmit }:
{ autofocus?: boolean, value: string, className?: string, placeholder: string, id?: number, setValue: (v: string) => void, onSubmit?: () => void }) {
return (<input
autoFocus={autofocus}
placeholder={placeholder}
value={value}
onKeyDown={(event) => {
if (event.key === 'Enter' && onSubmit) {
onSubmit()
}
}}
onChange={(event) => {
setValue(event.target.value)
}}
className={'bg-secondary w-full py-2 px-4 rounded-xl bg-w t-primary ' + className} />
className={'focus-visible:outline-none bg-secondary focus-visible:outline-theme w-full py-2 px-4 rounded-xl bg-w t-primary ' + className} />
)
}
export function Checkbox({ value, setValue, className, placeholder }: { value: boolean, className?: string, placeholder: string, id: string, setValue: React.Dispatch<React.SetStateAction<boolean>> }) {
export function Checkbox({ value, setValue, className, placeholder }:
{ value: boolean, className?: string, placeholder: string, id: string, setValue: React.Dispatch<React.SetStateAction<boolean>> }) {
return (<input type='checkbox'
placeholder={placeholder}
checked={value}
Expand Down
40 changes: 37 additions & 3 deletions client/src/page/feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function FeedPage({ id, TOC, clean }: { id: string, TOC: () => JSX.Elemen
const [_, setLocation] = useLocation();
const { showAlert, AlertUI } = useAlert();
const { showConfirm, ConfirmUI } = useConfirm();
const [top, setTop] = useState<number>(0);
const config = useContext(ClientConfigContext);
const counterEnabled = config.get<boolean>('counter.enabled');
function deleteFeed() {
Expand All @@ -69,6 +70,31 @@ export function FeedPage({ id, TOC, clean }: { id: string, TOC: () => JSX.Elemen
});
})
}
function topFeed() {
const topNew = top === 0 ? 1 : 0;
// Confirm
showConfirm(
topNew === 1 ? t("article.top.title") : t("article.untop.title"),
topNew === 1 ? t("article.top.confirm") : t("article.untop.confirm"),
() => {
if (!feed) return;
client
.feed.top({ id: feed.id })
.post({
top: topNew,
}, {
headers: headersWithAuth(),
})
.then(({ error }) => {
if (error) {
showAlert(error.value as string);
} else {
showAlert(topNew === 1 ? t("article.top.success") : t("article.untop.success"));
setTop(topNew);
}
});
})
}
useEffect(() => {
if (ref.current == id) return;
setFeed(undefined);
Expand All @@ -85,6 +111,7 @@ export function FeedPage({ id, TOC, clean }: { id: string, TOC: () => JSX.Elemen
} else if (data && typeof data !== "string") {
setTimeout(() => {
setFeed(data);
setTop(data.top);
// Extract head image
const img_reg = /!\[.*?\]\((.*?)\)/;
const img_match = img_reg.exec(data.content);
Expand Down Expand Up @@ -196,17 +223,24 @@ export function FeedPage({ id, TOC, clean }: { id: string, TOC: () => JSX.Elemen
<div className="pt-2">
{profile?.permission && (
<div className="flex gap-2">
<button
aria-label={top === 0 ? t("top.title") : t("untop.title")}
onClick={topFeed}
className={`flex-1 flex flex-col items-end justify-center px-2 py bg-hover rounded-full transition ${top > 0 ? "bg-theme text-white" : "bg-secondary dark:text-neutral-400"}`}
>
<i className="ri-skip-up-line" />
</button>
<Link
aria-label={t("edit")}
href={`/writing/${feed.id}`}
className="flex-1 flex flex-col items-end justify-center px-2 py bg-secondary hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-full transition"
className="flex-1 flex flex-col items-end justify-center px-2 py bg-secondary bg-hover bg-active rounded-full transition"
>
<i className="ri-edit-2-line dark:text-gray-400" />
<i className="ri-edit-2-line dark:text-neutral-400" />
</Link>
<button
aria-label={t("delete.title")}
onClick={deleteFeed}
className="flex-1 flex flex-col items-end justify-center px-2 py bg-secondary hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-full transition"
className="flex-1 flex flex-col items-end justify-center px-2 py bg-secondary bg-hover bg-active rounded-full transition"
>
<i className="ri-delete-bin-7-line text-red-500" />
</button>
Expand Down
Loading

0 comments on commit 547109e

Please sign in to comment.