add community page

This commit is contained in:
an-lee
2024-01-11 17:10:00 +08:00
parent 551b848ade
commit 94d4a0a338
19 changed files with 604 additions and 33 deletions

View File

@@ -10,6 +10,9 @@ export * from "./videos";
export * from "./medias";
export * from "./posts";
export * from "./users";
export * from "./db-state";
export * from "./layout";

View File

@@ -1,16 +1,14 @@
import { Button, useToast } from "@renderer/components/ui";
import { useContext, useState, useEffect } from "react";
import { WEB_API_URL } from "@/constants";
import { useContext, useEffect } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { t } from "i18next";
export const LoginForm = () => {
const { toast } = useToast();
const { EnjoyApp, login } = useContext(AppSettingsProviderContext);
const [endpoint, setEndpoint] = useState(WEB_API_URL);
const { EnjoyApp, login, apiUrl } = useContext(AppSettingsProviderContext);
const handleMixinLogin = () => {
const url = `${endpoint}/sessions/new?provider=mixin`;
const url = `${apiUrl}/sessions/new?provider=mixin`;
EnjoyApp.view.load(url, { x: 0, y: 0 });
};
@@ -36,7 +34,7 @@ export const LoginForm = () => {
const provider = new URL(url).pathname.split("/")[2];
const code = new URL(url).searchParams.get("code");
if (!url.startsWith(endpoint)) {
if (!url.startsWith(apiUrl)) {
toast({
title: t("error"),
description: t("invalidRedirectUrl"),
@@ -65,12 +63,6 @@ export const LoginForm = () => {
}
};
useEffect(() => {
EnjoyApp.app.apiUrl().then((url) => {
setEndpoint(url);
});
}, []);
useEffect(() => {
EnjoyApp.view.onViewState((_event, state) => onViewState(state));
@@ -78,7 +70,7 @@ export const LoginForm = () => {
EnjoyApp.view.removeViewStateListeners();
EnjoyApp.view.remove();
};
}, [endpoint]);
}, [apiUrl]);
return (
<div className="w-full max-w-sm px-6 flex flex-col space-y-4">

View File

@@ -0,0 +1 @@
export * from './posts';

View File

@@ -0,0 +1,37 @@
import { useContext, useEffect, useState } from "react";
import { Client } from "@/api";
import { AppSettingsProviderContext } from "@renderer/context";
import { t } from "i18next";
export const Posts = () => {
const { apiUrl, user } = useContext(AppSettingsProviderContext);
const [posts, setPosts] = useState<PostType[]>([]);
const client = new Client({
baseUrl: apiUrl,
accessToken: user.accessToken,
});
const fetchPosts = async () => {
client.posts().then(
(res) => {
setPosts(res.posts);
},
(err) => {
console.error(err);
}
);
};
useEffect(() => {
fetchPosts();
}, []);
return (
<div className="">
{posts.length === 0 && (
<div className="text-center text-gray-500">{t("noOneSharedYet")}</div>
)}
</div>
);
};

View File

@@ -14,6 +14,7 @@ import {
BookMarkedIcon,
UserIcon,
BotIcon,
UsersRoundIcon,
} from "lucide-react";
import { useLocation, Link } from "react-router-dom";
import { t } from "i18next";
@@ -50,6 +51,21 @@ export const Sidebar = () => {
<span className="hidden xl:block">{t("sidebar.home")}</span>
</Button>
</Link>
<Link
to="/community"
data-tooltip-id="sidebar-tooltip"
data-tooltip-content={t("sidebar.community")}
className="block"
>
<Button
variant={activeTab === "" ? "secondary" : "ghost"}
className="w-full xl:justify-start"
>
<UsersRoundIcon className="xl:mr-2 h-5 w-5" />
<span className="hidden xl:block">{t("sidebar.community")}</span>
</Button>
</Link>
</div>
</div>

View File

@@ -0,0 +1 @@
export * from './users-rankings';

View File

@@ -0,0 +1,89 @@
import { useContext, useEffect, useState } from "react";
import {
Avatar,
AvatarImage,
AvatarFallback,
Card,
CardTitle,
CardHeader,
CardContent,
} from "@renderer/components/ui";
import { Client } from "@/api";
import { AppSettingsProviderContext } from "@renderer/context";
import { t } from "i18next";
import { formatDuration } from "@renderer/lib/utils";
export const UsersRankings = () => {
return (
<div className="grid grid-cols-2 gap-6 mb-6">
<RankingsCard range="day" />
<RankingsCard range="week" />
<RankingsCard range="month" />
<RankingsCard range="all" />
</div>
);
};
const RankingsCard = (props: {
range: "day" | "week" | "month" | "year" | "all";
}) => {
const { range } = props;
const { apiUrl, user } = useContext(AppSettingsProviderContext);
const [rankings, setRankings] = useState<UserType[]>([]);
const client = new Client({
baseUrl: apiUrl,
accessToken: user.accessToken,
});
const fetchRankings = async () => {
client.rankings(range).then(
(res) => {
setRankings(res.rankings);
},
(err) => {
console.error(err);
}
);
};
useEffect(() => {
fetchRankings();
}, []);
return (
<Card>
<CardHeader>
<CardTitle>{t(`${range}Rankings`)}</CardTitle>
</CardHeader>
<CardContent>
{rankings.length === 0 && (
<div className="text-center text-gray-500">
{t("noOneHasRecordedYet")}
</div>
)}
{rankings.map((user, index) => (
<div key={user.id} className="flex items-center space-x-4 px-4 py-2">
<div className="font-mono text-sm">#{index + 1}</div>
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage src={user.avatarUrl} />
<AvatarFallback className="text-xl">
{user.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="max-w-20 truncate">{user.name}</div>
</div>
<div className="flex-1 font-serif text-right">
{formatDuration(user.recordingsDuration, "millisecond")}
</div>
</div>
))}
</CardContent>
</Card>
);
};

View File

@@ -1,6 +1,8 @@
import { createContext, useEffect, useState } from "react";
import { WEB_API_URL } from "@/constants";
type AppSettingsProviderState = {
apiUrl: string;
user: UserType | null;
initialized: boolean;
version?: string;
@@ -17,6 +19,7 @@ type AppSettingsProviderState = {
};
const initialState: AppSettingsProviderState = {
apiUrl: WEB_API_URL,
user: null,
initialized: false,
};
@@ -31,6 +34,7 @@ export const AppSettingsProvider = ({
}) => {
const [initialized, setInitialized] = useState<boolean>(false);
const [version, setVersion] = useState<string>("");
const [apiUrl, setApiUrl] = useState<string>(WEB_API_URL);
const [user, setUser] = useState<UserType | null>(null);
const [libraryPath, setLibraryPath] = useState("");
const [whisperModelsPath, setWhisperModelsPath] = useState<string>("");
@@ -44,6 +48,7 @@ export const AppSettingsProvider = ({
fetchLibraryPath();
fetchModel();
fetchFfmpegConfig();
fetchApiUrl();
}, []);
useEffect(() => {
@@ -107,6 +112,11 @@ export const AppSettingsProvider = ({
setWhisperModel(whisperModel);
};
const fetchApiUrl = async () => {
const apiUrl = await EnjoyApp.app.apiUrl();
setApiUrl(apiUrl);
}
const setModelHandler = async (name: string) => {
await EnjoyApp.settings.setWhisperModel(name);
setWhisperModel(name);
@@ -123,6 +133,7 @@ export const AppSettingsProvider = ({
value={{
EnjoyApp,
version,
apiUrl,
user,
login,
logout,

View File

@@ -3,10 +3,12 @@ import { twMerge } from "tailwind-merge";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import relativeTime from "dayjs/plugin/relativeTime";
import duration, { type DurationUnitType } from "dayjs/plugin/duration";
import "dayjs/locale/en";
import "dayjs/locale/zh-cn";
import i18next, { t } from "i18next";
dayjs.extend(localizedFormat);
dayjs.extend(duration);
dayjs.extend(relativeTime);
export function cn(...inputs: ClassValue[]) {
@@ -18,6 +20,23 @@ export function secondsToTimestamp(seconds: number) {
return date.toISOString().substr(11, 8);
}
export function humanizeDuration(
duration: number,
unit: DurationUnitType = "second"
) {
dayjs.locale(i18next.resolvedLanguage?.toLowerCase() || "en");
return dayjs.duration(duration, unit).humanize();
}
export function formatDuration(
duration: number,
unit: DurationUnitType = "second",
format = "HH:mm:ss"
) {
dayjs.locale(i18next.resolvedLanguage?.toLowerCase() || "en");
return dayjs.duration(duration, unit).format(format);
}
export function bytesToSize(bytes: number) {
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
if (bytes === 0) {

View File

@@ -0,0 +1,41 @@
import {
Button,
Tabs,
TabsList,
TabsContent,
TabsTrigger,
} from "@renderer/components/ui";
import { UsersRankings, Posts } from "@renderer/components";
import { ChevronLeftIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { t } from "i18next";
export default () => {
const navigate = useNavigate();
return (
<div className="h-full max-w-5xl mx-auto px-4 py-6">
<div className="flex space-x-1 items-center mb-4">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ChevronLeftIcon className="w-5 h-5" />
</Button>
<span>{t("sidebar.community")}</span>
</div>
<Tabs defaultValue="activities">
<TabsList className="mb-6">
<TabsTrigger value="activities">{t("activities")}</TabsTrigger>
<TabsTrigger value="rankings">{t("rankings")}</TabsTrigger>
</TabsList>
<TabsContent value="activities">
<Posts />
</TabsContent>
<TabsContent value="rankings">
<UsersRankings />
</TabsContent>
</Tabs>
</div>
);
};

View File

@@ -14,6 +14,7 @@ import Story from "./pages/story";
import Books from "./pages/books";
import Profile from "./pages/profile";
import Home from "./pages/home";
import Community from "./pages/community";
import StoryPreview from "./pages/story-preview";
export default createHashRouter([
@@ -23,6 +24,10 @@ export default createHashRouter([
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{
path: "/community",
element: <Community />,
},
{
path: "/profile",
element: <Profile />,