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": [
"lemmy.ml",
"lemm.ee",
"lemmy.world",
"feddit.it",
"lemmygrad.ml",
"reddthat.com",
"hexbear.net",

View file

@ -15,6 +15,13 @@ export const BACKGROUND_GRADIENT_1 =
export const BACKGROUND_GRADIENT_2 =
"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 }) => (
<div
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
export enum InstanceCategory {
Tech,
Sports,
// DO this as an interface and const list
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 {
domain: string;
languages: string[];
categories: InstanceCategory[];
categories: Category[];
}
// TODO fix these up
export const RECOMMENDED_INSTANCES: RecommendedInstance[] = [
{
domain: "lemmy.ml",
languages: ["en"],
categories: [TECH],
},
{
domain: "lemmy.world",
languages: ["en"],
categories: [TECH],
},
{
domain: "lemmy.fmhy.ml",
languages: ["en"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "discuss.tchncs.de",
languages: ["en"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "lemm.ee",
languages: ["en"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "reddthat.com",
languages: ["en"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "discuss.online",
languages: ["en"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "feddit.dk",
languages: ["da"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "feddit.de",
languages: ["de"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "discuss.tchncs.de",
languages: ["de"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "feddit.nl",
languages: ["nl"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "lemmy.pt",
languages: ["pt", "pt-PT", "pt-BR"],
categories: [InstanceCategory.Tech],
},
{
domain: "lemmy.pt",
languages: ["pt", "pt-PT", "pt-BR"],
categories: [InstanceCategory.Tech],
languages: ["pt"],
categories: [TECH],
},
{
domain: "lemmy.eus",
languages: ["eu"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "tabinezumi.net",
languages: ["ja"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "lm.korako.me",
languages: ["ja"],
categories: [InstanceCategory.Tech],
categories: [TECH],
},
{
domain: "feddit.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 { i18n } from "../i18next";
import { i18n, LANGUAGES } from "../i18next";
import { T } from "inferno-i18next";
import { instance_stats } from "../instance_stats";
import { mdToHtml, numToSI } from "../utils";
import { Badge, TEXT_GRADIENT } from "./common";
import { INSTANCE_HELPERS } from "./instances-definitions";
import { Badge, SELECT_CLASSES, TEXT_GRADIENT, languageList } from "./common";
import {
INSTANCE_HELPERS,
Category,
RECOMMENDED_INSTANCES,
All_CATEGORY,
CATEGORIES,
} from "./instances-definitions";
import { Icon } from "./icon";
const TitleBlock = () => (
@ -58,15 +64,15 @@ interface InstanceCardGridProps {
instances: any[];
}
const InstanceCardGrid = ({ title, instances }: InstanceCardGridProps) => (
<div className="my-16">
<SectionTitle title={title} />
// TODO create the instance picker helper
// - Language, Categories, and Sort Order (active, random)
const InstanceCardGrid = ({ instances }: InstanceCardGridProps) => (
<div className="grid md:grid-cols-3 grid-cols-1 gap-4">
{instances.map(i => (
<InstanceCard instance={i} />
))}
</div>
</div>
);
interface InstanceCardProps {
@ -238,77 +244,98 @@ export const DetailsModal = ({
</dialog>
);
function biasedRandom(activeUsers: number, avg: number, max: number): number {
// Lets introduce a better bias to random shuffle instances list
const influence = 1.25;
const rnd = Math.random() * (max / influence) + activeUsers;
const mix = Math.random() * influence;
return rnd * (1 - mix) + avg * mix;
interface Sort {
name: string;
icon: string;
}
function average(arr: number[]): number {
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
const RANDOM_SORT: Sort = {
name: "random",
icon: "TBD",
};
interface InstanceList {
recommended: any[];
popular: any[];
}
const MOST_ACTIVE_SORT: Sort = {
name: "most_active",
icon: "TBD",
};
function buildInstanceList(): InstanceList {
const recommendedList =
instance_stats.recommended[i18n.language] ??
instance_stats.recommended["en"];
const LEAST_ACTIVE_SORT: Sort = {
name: "least_active",
icon: "TBD",
};
let recommended = [];
let popular = [];
let usersActiveMonth = [];
const SORTS: Sort[] = [RANDOM_SORT, MOST_ACTIVE_SORT, LEAST_ACTIVE_SORT];
// Loop over all the instances, and add them to the two lists
for (const i of instance_stats.stats.instance_details) {
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
function sortRandom(instances: any[]): any[] {
return instances
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.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) {
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() {
const title = i18n.t("join_title");
const instances = buildInstanceList();
return (
<div className="container mx-auto">
@ -317,49 +344,84 @@ export class Instances extends Component<any, any> {
</Helmet>
<TitleBlock />
<ComparisonBlock />
<InstanceCardGrid
title={i18n.t("recommended_instances")}
instances={instances.recommended}
/>
{this.filterAndTitleBlock()}
<InstanceCardGrid
title={i18n.t("popular_instances")}
instances={instances.popular}
instances={this.buildInstanceList()}
/>
</div>
);
}
renderList(header: string, instances: any[]) {
// TODO i18n these
filterAndTitleBlock() {
return (
<div>
<h2>{header}</h2>
<div class="row">
{instances.map(instance => {
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 className="my-16">
<div className="flex flex-row flex-wrap gap-4">
<div className="flex-none">
<SectionTitle title={i18n.t("join_title")} />
</div>
</header>
<div class="is-center">
<img class="join-banner" src={icon} onError={imgError} />
<div className="grow"></div>
<div className="flex-none">
<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>
<br />
<p class="join-desc">{description}</p>
<footer>
<a class="button primary" href={`https://${domain}`}>
{i18n.t("browse_instance")}
</a>
</footer>
</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 { Link } from "inferno-router";
import { Icon, IconSize } from "./icon";
import { i18n, languages } from "../i18next";
import { i18n, LANGUAGES } from "../i18next";
import classNames from "classnames";
import { SELECT_CLASSES, languageList } from "./common";
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>) {
location.href = `/?lang=${event.target.value}`;
}
@ -52,7 +49,7 @@ export const Navbar = ({ footer = false }) => (
<>
<select
onChange={linkEvent(this, handleLanguageChange)}
class="select select-sm select-ghost select-bordered text-gray-400"
className={SELECT_CLASSES}
>
{languageList().map((language, i) => (
<option
@ -60,7 +57,7 @@ export const Navbar = ({ footer = false }) => (
value={language}
selected={i18n.language.startsWith(language)}
>
{languages.find(l => l.code.startsWith(language)).name}
{LANGUAGES.find(l => l.code.startsWith(language)).name}
</option>
))}
</select>

View file

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