Starting to add filters.

This commit is contained in:
Dessalines 2023-10-08 23:03:38 -04:00
parent 1ad0188eec
commit c59234a7f7
8 changed files with 226 additions and 136 deletions

@ -1 +1 @@
Subproject commit b95dfdad19b866e86ed810d6f79c99e13066671e Subproject commit afcb1684e80a42a6fecd6c0cebdf4caa64f92440

@ -1 +1 @@
Subproject commit 0a5b9790fbae41e8dc51021c2d20ec0f5743b1d8 Subproject commit b9a566d8376c1ca4d122d63cb921accc3db6c79f

View file

@ -2,6 +2,8 @@
"recommended": [ "recommended": [
"lemmy.ml", "lemmy.ml",
"lemm.ee", "lemm.ee",
"lemmy.world",
"feddit.it",
"lemmygrad.ml", "lemmygrad.ml",
"reddthat.com", "reddthat.com",
"hexbear.net", "hexbear.net",

View file

@ -15,6 +15,13 @@ export const BACKGROUND_GRADIENT_1 =
export const BACKGROUND_GRADIENT_2 = export const BACKGROUND_GRADIENT_2 =
"min-h-full bg-gradient-to-b from-transparent to-black/[.30] to-20%"; "min-h-full bg-gradient-to-b from-transparent to-black/[.30] to-20%";
export const SELECT_CLASSES =
"select select-sm select-ghost select-bordered text-gray-400";
export function languageList() {
return Object.keys(i18n.services.resourceStore.data).sort();
}
export const Badge = ({ content, outline = false }) => ( export const Badge = ({ content, outline = false }) => (
<div <div
className={classNames("p-2 rounded-xl bg-neutral-800 text-gray-300 w-fit", { className={classNames("p-2 rounded-xl bg-neutral-800 text-gray-300 w-fit", {

View file

@ -31,99 +31,121 @@ export const INSTANCE_HELPERS: InstanceHelper[] = [
]; ];
// TODO add i18n strings, Icons // TODO add i18n strings, Icons
export enum InstanceCategory { // DO this as an interface and const list
Tech,
Sports, export interface Category {
name: string;
icon: string;
} }
export const All_CATEGORY: Category = {
name: "all",
icon: "TBD",
};
export const SPORTS: Category = {
name: "sports",
icon: "TBD",
};
export const TECH: Category = {
name: "tech",
icon: "TBD",
};
export const CATEGORIES: Category[] = [All_CATEGORY, TECH, SPORTS];
export interface RecommendedInstance { export interface RecommendedInstance {
domain: string; domain: string;
languages: string[]; languages: string[];
categories: InstanceCategory[]; categories: Category[];
} }
// TODO fix these up // TODO fix these up
export const RECOMMENDED_INSTANCES: RecommendedInstance[] = [ export const RECOMMENDED_INSTANCES: RecommendedInstance[] = [
{
domain: "lemmy.ml",
languages: ["en"],
categories: [TECH],
},
{
domain: "lemmy.world",
languages: ["en"],
categories: [TECH],
},
{ {
domain: "lemmy.fmhy.ml", domain: "lemmy.fmhy.ml",
languages: ["en"], languages: ["en"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "discuss.tchncs.de", domain: "discuss.tchncs.de",
languages: ["en"], languages: ["en"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "lemm.ee", domain: "lemm.ee",
languages: ["en"], languages: ["en"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "reddthat.com", domain: "reddthat.com",
languages: ["en"], languages: ["en"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "discuss.online", domain: "discuss.online",
languages: ["en"], languages: ["en"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "feddit.dk", domain: "feddit.dk",
languages: ["da"], languages: ["da"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "feddit.de", domain: "feddit.de",
languages: ["de"], languages: ["de"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "discuss.tchncs.de", domain: "discuss.tchncs.de",
languages: ["de"], languages: ["de"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "feddit.nl", domain: "feddit.nl",
languages: ["nl"], languages: ["nl"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "lemmy.pt", domain: "lemmy.pt",
languages: ["pt", "pt-PT", "pt-BR"], languages: ["pt"],
categories: [InstanceCategory.Tech], categories: [TECH],
},
{
domain: "lemmy.pt",
languages: ["pt", "pt-PT", "pt-BR"],
categories: [InstanceCategory.Tech],
}, },
{ {
domain: "lemmy.eus", domain: "lemmy.eus",
languages: ["eu"], languages: ["eu"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "tabinezumi.net", domain: "tabinezumi.net",
languages: ["ja"], languages: ["ja"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "lm.korako.me", domain: "lm.korako.me",
languages: ["ja"], languages: ["ja"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
{ {
domain: "feddit.it", domain: "feddit.it",
languages: ["it"], languages: ["it"],
categories: [InstanceCategory.Tech], categories: [TECH],
}, },
]; ];

View file

@ -1,11 +1,17 @@
import { Component, InfernoEventHandler } from "inferno"; import { Component, InfernoEventHandler, linkEvent } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { i18n } from "../i18next"; import { i18n, LANGUAGES } from "../i18next";
import { T } from "inferno-i18next"; import { T } from "inferno-i18next";
import { instance_stats } from "../instance_stats"; import { instance_stats } from "../instance_stats";
import { mdToHtml, numToSI } from "../utils"; import { mdToHtml, numToSI } from "../utils";
import { Badge, TEXT_GRADIENT } from "./common"; import { Badge, SELECT_CLASSES, TEXT_GRADIENT, languageList } from "./common";
import { INSTANCE_HELPERS } from "./instances-definitions"; import {
INSTANCE_HELPERS,
Category,
RECOMMENDED_INSTANCES,
All_CATEGORY,
CATEGORIES,
} from "./instances-definitions";
import { Icon } from "./icon"; import { Icon } from "./icon";
const TitleBlock = () => ( const TitleBlock = () => (
@ -58,15 +64,15 @@ interface InstanceCardGridProps {
instances: any[]; instances: any[];
} }
const InstanceCardGrid = ({ title, instances }: InstanceCardGridProps) => ( // TODO create the instance picker helper
<div className="my-16">
<SectionTitle title={title} /> // - Language, Categories, and Sort Order (active, random)
const InstanceCardGrid = ({ instances }: InstanceCardGridProps) => (
<div className="grid md:grid-cols-3 grid-cols-1 gap-4"> <div className="grid md:grid-cols-3 grid-cols-1 gap-4">
{instances.map(i => ( {instances.map(i => (
<InstanceCard instance={i} /> <InstanceCard instance={i} />
))} ))}
</div> </div>
</div>
); );
interface InstanceCardProps { interface InstanceCardProps {
@ -238,77 +244,98 @@ export const DetailsModal = ({
</dialog> </dialog>
); );
function biasedRandom(activeUsers: number, avg: number, max: number): number { interface Sort {
// Lets introduce a better bias to random shuffle instances list name: string;
const influence = 1.25; icon: string;
const rnd = Math.random() * (max / influence) + activeUsers;
const mix = Math.random() * influence;
return rnd * (1 - mix) + avg * mix;
} }
function average(arr: number[]): number { const RANDOM_SORT: Sort = {
return arr.reduce((a, b) => a + b, 0) / arr.length; name: "random",
} icon: "TBD",
};
interface InstanceList { const MOST_ACTIVE_SORT: Sort = {
recommended: any[]; name: "most_active",
popular: any[]; icon: "TBD",
} };
function buildInstanceList(): InstanceList { const LEAST_ACTIVE_SORT: Sort = {
const recommendedList = name: "least_active",
instance_stats.recommended[i18n.language] ?? icon: "TBD",
instance_stats.recommended["en"]; };
let recommended = []; const SORTS: Sort[] = [RANDOM_SORT, MOST_ACTIVE_SORT, LEAST_ACTIVE_SORT];
let popular = [];
let usersActiveMonth = [];
// Loop over all the instances, and add them to the two lists function sortRandom(instances: any[]): any[] {
for (const i of instance_stats.stats.instance_details) { return instances
if (recommendedList.indexOf(i.domain) > -1) {
recommended.push(i);
} else {
popular.push(i);
}
usersActiveMonth.push(i.site_info.site_view.counts.users_active_month);
}
// Use these values for the shuffle
const avgMonthlyUsers = average(usersActiveMonth);
const maxMonthlyUsers = Math.max(...usersActiveMonth);
// Randomize the recommended
recommended = recommended
.map(value => ({ value, sort: Math.random() })) .map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort) .sort((a, b) => a.sort - b.sort)
.map(({ value }) => value); .map(({ value }) => value);
// BIASED sorting for instances, based on the min/max of users_active_month
popular = popular
.map(i => ({
instance: i,
sort: biasedRandom(
i.site_info.site_view.counts.users_active_month,
avgMonthlyUsers,
maxMonthlyUsers,
),
}))
.sort((a, b) => b.sort - a.sort)
.map(({ instance }) => instance);
return { recommended, popular };
} }
export class Instances extends Component<any, any> { function sortActive(instances: any[]): any[] {
return instances.sort(
(a, b) =>
b.site_info.site_view.counts.users_active_month -
a.site_info.site_view.counts.users_active_month,
);
}
interface State {
sort: Sort;
language: string;
category: Category;
}
export class Instances extends Component<any, State> {
state: State = {
sort: SORTS[0],
language: i18n.language.split("-")[0],
category: All_CATEGORY,
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
buildInstanceList(): any[] {
let instances = instance_stats.stats.instance_details;
const recommended = RECOMMENDED_INSTANCES;
// Language Filter
if (this.state.language !== "all") {
const languageRecs = recommended.filter(r =>
r.languages.includes(this.state.language),
);
instances = instances.filter(i =>
languageRecs.map(r => r.domain).includes(i.domain),
);
}
// Category filter
if (this.state.category !== All_CATEGORY) {
const categoryRecs = recommended.filter(r =>
r.categories.includes(this.state.category),
);
instances = instances.filter(i =>
categoryRecs.map(c => c.domain).includes(i.domain),
);
}
// Sort
if (this.state.sort == SORTS[0]) {
instances = sortRandom(instances);
} else if (this.state.sort == SORTS[1]) {
instances = sortActive(instances);
} else {
instances = sortActive(instances).reverse();
}
return instances;
}
render() { render() {
const title = i18n.t("join_title"); const title = i18n.t("join_title");
const instances = buildInstanceList();
return ( return (
<div className="container mx-auto"> <div className="container mx-auto">
@ -317,49 +344,84 @@ export class Instances extends Component<any, any> {
</Helmet> </Helmet>
<TitleBlock /> <TitleBlock />
<ComparisonBlock /> <ComparisonBlock />
<InstanceCardGrid {this.filterAndTitleBlock()}
title={i18n.t("recommended_instances")}
instances={instances.recommended}
/>
<InstanceCardGrid <InstanceCardGrid
title={i18n.t("popular_instances")} title={i18n.t("popular_instances")}
instances={instances.popular} instances={this.buildInstanceList()}
/> />
</div> </div>
); );
} }
renderList(header: string, instances: any[]) { // TODO i18n these
filterAndTitleBlock() {
return ( return (
<div> <div className="my-16">
<h2>{header}</h2> <div className="flex flex-row flex-wrap gap-4">
<div class="row"> <div className="flex-none">
{instances.map(instance => { <SectionTitle title={i18n.t("join_title")} />
let domain = instance.domain;
let description = instance.site_info.site_view.site.description;
let icon = instance.site_info.site_view.site.icon;
return (
<div class="card col-6">
<header>
<div class="row">
<h4 class="col">{domain}</h4>
</div> </div>
</header> <div className="grow"></div>
<div class="is-center"> <div className="flex-none">
<img class="join-banner" src={icon} onError={imgError} /> <select
className={`${SELECT_CLASSES} mr-2`}
value={this.state.category.name}
onChange={linkEvent(this, handleCategoryChange)}
name="category_select"
>
<option disabled selected>
Category
</option>
{CATEGORIES.map(c => (
<option key={c.name} value={c.name}>
{c.name}
</option>
))}
</select>
<select
value={this.state.language}
onChange={linkEvent(this, handleLanguageChange)}
className={`${SELECT_CLASSES} mr-2`}
>
<option disabled>Languages</option>
<option key="all" value="all">
all
</option>
{languageList().map((language, i) => (
<option key={i} value={language}>
{LANGUAGES.find(l => l.code.startsWith(language)).name}
</option>
))}
</select>
<select
value={this.state.sort.name}
name="sort_select"
className={SELECT_CLASSES}
onChange={linkEvent(this, handleSortChange)}
>
<option disabled>Sort TODO</option>
{SORTS.map(s => (
<option key={s.name} value={s.name}>
{s.name}
</option>
))}
</select>
</div> </div>
<br />
<p class="join-desc">{description}</p>
<footer>
<a class="button primary" href={`https://${domain}`}>
{i18n.t("browse_instance")}
</a>
</footer>
</div>
);
})}
</div> </div>
</div> </div>
); );
} }
} }
function handleSortChange(i: Instances, event: any) {
i.setState({ sort: SORTS.find(s => s.name == event.target.value) });
}
function handleCategoryChange(i: Instances, event: any) {
i.setState({ category: CATEGORIES.find(c => c.name == event.target.value) });
}
function handleLanguageChange(i: Instances, event: any) {
i.setState({ language: event.target.value });
}

View file

@ -1,8 +1,9 @@
import { ChangeEvent, linkEvent } from "inferno"; import { ChangeEvent, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { Icon, IconSize } from "./icon"; import { Icon, IconSize } from "./icon";
import { i18n, languages } from "../i18next"; import { i18n, LANGUAGES } from "../i18next";
import classNames from "classnames"; import classNames from "classnames";
import { SELECT_CLASSES, languageList } from "./common";
const NavLink = ({ content }) => <li className="text-gray-400">{content}</li>; const NavLink = ({ content }) => <li className="text-gray-400">{content}</li>;
@ -17,10 +18,6 @@ const NavLinks = () => (
</> </>
); );
function languageList() {
return Object.keys(i18n.services.resourceStore.data).sort();
}
function handleLanguageChange(_: any, event: ChangeEvent<HTMLSelectElement>) { function handleLanguageChange(_: any, event: ChangeEvent<HTMLSelectElement>) {
location.href = `/?lang=${event.target.value}`; location.href = `/?lang=${event.target.value}`;
} }
@ -52,7 +49,7 @@ export const Navbar = ({ footer = false }) => (
<> <>
<select <select
onChange={linkEvent(this, handleLanguageChange)} onChange={linkEvent(this, handleLanguageChange)}
class="select select-sm select-ghost select-bordered text-gray-400" className={SELECT_CLASSES}
> >
{languageList().map((language, i) => ( {languageList().map((language, i) => (
<option <option
@ -60,7 +57,7 @@ export const Navbar = ({ footer = false }) => (
value={language} value={language}
selected={i18n.language.startsWith(language)} selected={i18n.language.startsWith(language)}
> >
{languages.find(l => l.code.startsWith(language)).name} {LANGUAGES.find(l => l.code.startsWith(language)).name}
</option> </option>
))} ))}
</select> </select>

View file

@ -25,7 +25,7 @@ import { pt_BR } from "./translations/pt_BR";
import { ru } from "./translations/ru"; import { ru } from "./translations/ru";
import { zh } from "./translations/zh"; import { zh } from "./translations/zh";
export const languages = [ export const LANGUAGES = [
{ resource: bg, code: "bg", name: "Български" }, { resource: bg, code: "bg", name: "Български" },
{ resource: da, code: "da", name: "Dansk" }, { resource: da, code: "da", name: "Dansk" },
{ resource: de, code: "de", name: "Deutsch" }, { resource: de, code: "de", name: "Deutsch" },
@ -52,7 +52,7 @@ export const languages = [
]; ];
const resources: Resource = {}; const resources: Resource = {};
languages.forEach(l => (resources[l.code] = l.resource)); LANGUAGES.forEach(l => (resources[l.code] = l.resource));
function format(value: any, format: any): any { function format(value: any, format: any): any {
return format === "uppercase" ? value.toUpperCase() : value; return format === "uppercase" ? value.toUpperCase() : value;