Skip to content

Commit

Permalink
Merge pull request #206 from ecency/feature/page-views
Browse files Browse the repository at this point in the history
Added statistics for posts
  • Loading branch information
feruzm authored Dec 14, 2024
2 parents 189dc41 + 7e9228e commit 8594e95
Show file tree
Hide file tree
Showing 22 changed files with 391 additions and 185 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
export HIVESIGNER_SECRET=$HIVESIGNER_SECRET
export SEARCH_API_ADDR=$SEARCH_API_ADDR
export SEARCH_API_SECRET=$SEARCH_API_SECRET
export PLAUSIBLE_API_KEY=$PLAUSIBLE_API_KEY
cd ~/vision-next
git pull origin main
docker system prune -f
Expand Down Expand Up @@ -110,6 +111,7 @@ jobs:
export HIVESIGNER_SECRET=$HIVESIGNER_SECRET
export SEARCH_API_ADDR=$SEARCH_API_ADDR
export SEARCH_API_SECRET=$SEARCH_API_SECRET
export PLAUSIBLE_API_KEY=$PLAUSIBLE_API_KEY
cd ~/vision-next
git pull origin main
docker system prune -f
Expand Down Expand Up @@ -144,6 +146,7 @@ jobs:
export HIVESIGNER_SECRET=$HIVESIGNER_SECRET
export SEARCH_API_ADDR=$SEARCH_API_ADDR
export SEARCH_API_SECRET=$SEARCH_API_SECRET
export PLAUSIBLE_API_KEY=$PLAUSIBLE_API_KEY
cd ~/vision-next
git pull origin main
docker system prune -f
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
export HIVESIGNER_SECRET=$HIVESIGNER_SECRET
export SEARCH_API_ADDR=$SEARCH_API_ADDR
export SEARCH_API_SECRET=$SEARCH_API_SECRET
export PLAUSIBLE_API_KEY=$PLAUSIBLE_API_KEY
cd ~/vision-next
git pull origin develop
docker pull ecency/vision-next:develop
Expand Down
1 change: 1 addition & 0 deletions src/api/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ export * from "./get-chain-properties-query";
export * from "./get-gifs-query";
export * from "./spk";
export * from "./engine";
export * from "./stats";
36 changes: 36 additions & 0 deletions src/api/queries/stats/get-page-stats-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { EcencyQueriesManager, QueryIdentifiers } from "@/core/react-query";
import { appAxios } from "@/api/axios";

export interface StatsResponse {
results: [
{
metrics: number[];
dimensions: string[];
}
];
query: {
site_id: string;
metrics: string[];
date_range: string[];
filters: unknown[];
};
}

export function useGetStatsQuery(
url: string,
dimensions: string[] = [],
metrics = ["visitors", "pageviews", "visit_duration"]
) {
return EcencyQueriesManager.generateClientServerQuery({
queryKey: [QueryIdentifiers.PAGE_STATS, url, dimensions, metrics],
queryFn: async () => {
const response = await appAxios.post<StatsResponse>(`/api/stats`, {
metrics,
url: encodeURIComponent(url),
dimensions
});
return response.data;
},
enabled: !!url
});
}
1 change: 1 addition & 0 deletions src/api/queries/stats/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./get-page-stats-query";
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { accountReputation, parseDate } from "@/utils";
import { TagLink } from "@/features/shared/tag";
import { EntryPageMainInfoMenu } from "@/app/(dynamicPages)/entry/[category]/[author]/[permlink]/_components/entry-page-main-info-menu";
import { EcencyConfigManager } from "@/config";
import { EntryPageStats } from "@/app/(dynamicPages)/entry/[category]/[author]/[permlink]/_components/entry-page-stats";

interface Props {
entry: Entry;
Expand All @@ -19,56 +20,56 @@ export function EntryPageMainInfo({ entry }: Props) {
const reputation = accountReputation(entry.author_reputation ?? 0);

return (
<div className="entry-info">
<ProfileLink username={entry.author}>
<div className="author-avatar">
<UserAvatar username={entry.author} size="medium" />
</div>
</ProfileLink>

<div className="entry-info-inner">
<div className="info-line-1">
<div className="p-2 md:p-4 md:pb-3 border border-[--border-color] rounded-2xl flex flex-col gap-4 mb-4 md:mb-6 lg:mb-8 mt-2 lg:mt-4">
<div className="flex items-center gap-4">
<ProfileLink username={entry.author}>
<UserAvatar username={entry.author} size="sLarge" />
</ProfileLink>
<div className="flex flex-col gap-1">
<ProfileLink username={entry.author}>
<div className="author notranslate">
<span className="author-name">
<span itemProp="author" itemScope={true} itemType="http://schema.org/Person">
<span itemProp="name">{entry.author}</span>
</span>
</span>
<div
className="text-lg notranslate"
itemProp="author"
itemScope={true}
itemType="http://schema.org/Person"
>
<span itemProp="name">{entry.author}</span>
<span className="author-reputation" title={i18next.t("entry.author-reputation")}>
{reputation}
</span>
</div>
</ProfileLink>
</div>

<div className="info-line-2 gap-1">
<span className="date" title={published.format("LLLL")}>
{published.fromNow()}
</span>
<span className="separator circle-separator" />
<div className="entry-tag">
<span className="in-tag mr-2">{i18next.t("entry.community-in")}</span>
<div className="flex text-sm items-center">
<div className="in-tag mr-2 opacity-50">{i18next.t("entry.published")}</div>
<TagLink tag={entry.category} type="link">
<div className="tag-name">
{entry.community ? entry.community_title : `#${entry.category}`}
</div>
{entry.community ? entry.community_title : `#${entry.category}`}
</TagLink>
</div>
</div>
</div>
<span className="flex-spacer" />

<ReadTime entry={entry} toolTip={true} />

{!isComment && (
<EcencyConfigManager.Conditional
condition={({ visionFeatures }) => visionFeatures.bookmarks.enabled}
>
<BookmarkBtn entry={entry} />
</EcencyConfigManager.Conditional>
)}
{!isComment && <EntryPageMainInfoMenu entry={entry} />}
<div className="flex items-center justify-between">
<div className="flex items-center text-sm">
<ReadTime entry={entry} toolTip={true} />
<span className="separator circle-separator mx-1 lg:hidden" />
<EntryPageStats entry={entry} />
<span className="separator circle-separator mx-1" />
<div className="date" title={published.format("LLLL")}>
{published.fromNow()}
</div>
</div>
<div className="flex items-center justify-end">
{!isComment && (
<EcencyConfigManager.Conditional
condition={({ visionFeatures }) => visionFeatures.bookmarks.enabled}
>
<BookmarkBtn entry={entry} />
</EcencyConfigManager.Conditional>
)}
{!isComment && <EntryPageMainInfoMenu entry={entry} />}
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import i18next from "i18next";
import { useMemo } from "react";
import { useGetStatsQuery } from "@/api/queries";
import { AnimatePresence, motion } from "framer-motion";

interface Props {
totalViews: number;
cleanedPathname: string;
}

export function EntryPageStatsByCountries({ totalViews, cleanedPathname }: Props) {
const { data: stats } = useGetStatsQuery(cleanedPathname, [
"visit:country_name"
]).useClientQuery();

const countries = useMemo(
() =>
stats?.results?.reduce<Record<string, number>>((acc, result) => {
const country = result.dimensions[0];
const views = +result.metrics[1];
return { ...acc, [country]: (acc[country] ?? 0) + views };
}, {}) ?? {},
[stats?.results]
);
const countriesList = useMemo(
() => Object.entries(countries).sort((a, b) => b[1] - a[1]),
[countries]
);

return countriesList.length > 0 ? (
<div className="flex flex-col w-full gap-2 text-sm">
<div className="text-sm opacity-50 pb-2">{i18next.t("entry.stats.countries")}</div>
<AnimatePresence mode="popLayout">
{countriesList.map(([country, views]) => (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="rounded-md overflow-hidden relative w-full flex items-center justify-between bg-gray-100 dark:bg-gray-900"
key={country}
transition={{ delay: 0.2 }}
>
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(views * 100) / totalViews}%` }}
transition={{ delay: 0.3 }}
className="absolute h-full bg-gray-200 dark:bg-dark-default"
/>
<div className="relative pl-2 py-1">{country}</div>
<div className="relative pr-2 text-blue-dark-sky font-semibold">{views}</div>
</motion.div>
))}
</AnimatePresence>
</div>
) : (
<></>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import i18next from "i18next";
import { useMemo } from "react";
import { useGetStatsQuery } from "@/api/queries";
import { AnimatePresence, motion } from "framer-motion";

interface Props {
totalViews: number;
cleanedPathname: string;
}

export function EntryPageStatsByDevices({ totalViews, cleanedPathname }: Props) {
const { data: stats } = useGetStatsQuery(cleanedPathname, ["visit:device"]).useClientQuery();

const devices = useMemo(
() =>
stats?.results?.reduce<Record<string, number>>((acc, result) => {
const country = result.dimensions[0];
const views = +result.metrics[1];
return { ...acc, [country]: (acc[country] ?? 0) + views };
}, {}) ?? {},
[stats?.results]
);
const devicesList = useMemo(() => Object.entries(devices).sort((a, b) => b[1] - a[1]), [devices]);

return devicesList.length > 0 ? (
<div className="flex flex-col w-full gap-2 text-sm">
<div className="text-sm opacity-50 pb-2">{i18next.t("entry.stats.devices")}</div>
<AnimatePresence mode="popLayout">
{devicesList.map(([country, views]) => (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="rounded-md overflow-hidden relative w-full flex items-center justify-between bg-gray-100 dark:bg-gray-900"
key={country}
transition={{ delay: 0.2 }}
>
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(views * 100) / totalViews}%` }}
transition={{ delay: 0.3 }}
className="absolute h-full bg-gray-200 dark:bg-dark-default"
/>
<div className="relative pl-2 py-1">{country}</div>
<div className="relative pr-2 text-blue-dark-sky font-semibold">{views}</div>
</motion.div>
))}
</AnimatePresence>
</div>
) : (
<></>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { motion } from "framer-motion";
import { ReactNode } from "react";

interface Props {
count: ReactNode;
label: ReactNode;
}

export function EntryPageStatsItem({ count, label }: Props) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.875 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="flex flex-col items-center gap-1"
>
<div className="text-xl lg:text-2xl text-blue-dark-sky">{count}</div>
<div>{label}</div>
</motion.div>
);
}
Loading

0 comments on commit 8594e95

Please sign in to comment.