Finishing up helper modal.

This commit is contained in:
Dessalines 2023-10-11 16:47:34 -04:00
parent 709612005b
commit 7241dfaedf
11 changed files with 200 additions and 106 deletions

@ -1 +1 @@
Subproject commit afcb1684e80a42a6fecd6c0cebdf4caa64f92440 Subproject commit fd888ab941a2f77e30ebafda065289fa0542d23e

@ -1 +1 @@
Subproject commit b9a566d8376c1ca4d122d63cb921accc3db6c79f Subproject commit d0f3548379e446d2c333e582734bc68f8d684f4d

View file

@ -19,4 +19,8 @@ const wrapper = (
</BrowserRouter> </BrowserRouter>
); );
hydrate(wrapper, document.getElementById("root")); const root = document.getElementById("root");
if (root) {
hydrate(wrapper, root);
}

View file

@ -25,7 +25,7 @@ export class App extends Component<any, any> {
key={path} key={path}
path={path} path={path}
exact={exact} exact={exact}
render={props => <C {...props} {...rest} />} render={props => C && <C {...props} {...rest} />}
/> />
))} ))}
<Route render={props => <NoMatch {...props} />} /> <Route render={props => <NoMatch {...props} />} />

View file

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

View file

@ -3,16 +3,17 @@ import { Helmet } from "inferno-helmet";
import { i18n, LANGUAGES } 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 { languageList, mdToHtml, numToSI } from "../utils"; import { getQueryParams, mdToHtml, numToSI } from "../utils";
import { Badge, SELECT_CLASSES, TEXT_GRADIENT } from "./common"; import { Badge, SELECT_CLASSES, TEXT_GRADIENT } from "./common";
import { import {
INSTANCE_HELPERS, INSTANCE_HELPERS,
Category, Topic,
RECOMMENDED_INSTANCES, RECOMMENDED_INSTANCES,
All_CATEGORY, All_TOPIC,
CATEGORIES, TOPICS,
} from "./instances-definitions"; } from "./instances-definitions";
import { Icon } from "./icon"; import { Icon } from "./icon";
import { I18nKeys } from "i18next";
const TitleBlock = () => ( const TitleBlock = () => (
<div className="flex flex-col items-center pt-16 mb-16"> <div className="flex flex-col items-center pt-16 mb-16">
@ -64,9 +65,6 @@ interface InstanceCardGridProps {
instances: any[]; instances: any[];
} }
// TODO create the instance picker helper
// - Language, Categories, and Sort Order (active, random)
const InstanceCardGrid = ({ instances }: InstanceCardGridProps) => ( 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 => (
@ -282,23 +280,67 @@ function sortActive(instances: any[]): any[] {
} }
interface State { interface State {
instances: any[];
sort: Sort; sort: Sort;
language: string; language: string;
category: Category; topic: Topic;
scroll: boolean;
} }
export class Instances extends Component<any, State> { interface Props {
sort: Sort;
language: string;
topic: Topic;
scroll: boolean;
}
function getSortFromQuery(sort?: string): Sort {
return SORTS.find(s => s.name == sort) ?? RANDOM_SORT;
}
function getTopicFromQuery(topic?: string): Topic {
return TOPICS.find(c => c.name == topic) ?? All_TOPIC;
}
function getInstancesQueryParams() {
return getQueryParams<Props>({
sort: getSortFromQuery,
language: d => d || "all",
topic: getTopicFromQuery,
scroll: d => !!d,
});
}
export class Instances extends Component<Props, State> {
state: State = { state: State = {
sort: SORTS[0], instances: [],
language: i18n.language.split("-")[0], sort: RANDOM_SORT,
category: All_CATEGORY, language: "all",
topic: All_TOPIC,
scroll: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
buildInstanceList(): any[] { // Set the filters by the query params if they exist
componentDidMount(): void {
this.setState(getInstancesQueryParams());
this.buildInstanceList();
this.scrollToSearch();
}
scrollToSearch() {
if (this.state.scroll) {
const el = document.getElementById("search")?.offsetTop;
if (el) {
window.scrollTo({ top: el, behavior: "smooth" });
}
}
}
buildInstanceList() {
let instances = instance_stats.stats.instance_details; let instances = instance_stats.stats.instance_details;
const recommended = RECOMMENDED_INSTANCES; const recommended = RECOMMENDED_INSTANCES;
@ -312,26 +354,26 @@ export class Instances extends Component<any, State> {
); );
} }
// Category filter // Topic filter
if (this.state.category !== All_CATEGORY) { if (this.state.topic !== All_TOPIC) {
const categoryRecs = recommended.filter(r => const topicRecs = recommended.filter(r =>
r.categories.includes(this.state.category), r.topics.includes(this.state.topic),
); );
instances = instances.filter(i => instances = instances.filter(i =>
categoryRecs.map(c => c.domain).includes(i.domain), topicRecs.map(c => c.domain).includes(i.domain),
); );
} }
// Sort // Sort
if (this.state.sort == SORTS[0]) { if (this.state.sort == RANDOM_SORT) {
instances = sortRandom(instances); instances = sortRandom(instances);
} else if (this.state.sort == SORTS[1]) { } else if (this.state.sort == MOST_ACTIVE_SORT) {
instances = sortActive(instances); instances = sortActive(instances);
} else { } else {
instances = sortActive(instances).reverse(); instances = sortActive(instances).reverse();
} }
return instances; this.setState({ instances });
} }
render() { render() {
@ -345,18 +387,37 @@ export class Instances extends Component<any, State> {
<TitleBlock /> <TitleBlock />
<ComparisonBlock /> <ComparisonBlock />
{this.filterAndTitleBlock()} {this.filterAndTitleBlock()}
<div className="mt-4">
{this.state.instances.length > 0 ? (
<InstanceCardGrid <InstanceCardGrid
title={i18n.t("popular_instances")} title={i18n.t("popular_instances")}
instances={this.buildInstanceList()} instances={this.state.instances}
/> />
) : (
this.seeAllBtn()
)}
</div>
</div>
);
}
seeAllBtn() {
return (
<div>
<p className="text-sm text-gray-300 mb-4">{i18n.t("none_found")}</p>
<button
className="btn btn-sm btn-secondary text-white normal-case"
onClick={linkEvent(this, handleSeeAll)}
>
{i18n.t("see_all_servers")}
</button>
</div> </div>
); );
} }
// TODO i18n these
filterAndTitleBlock() { filterAndTitleBlock() {
return ( return (
<div className="my-16"> <div id="search" className="mt-16">
<div className="flex flex-row flex-wrap gap-4"> <div className="flex flex-row flex-wrap gap-4">
<div className="flex-none"> <div className="flex-none">
<SectionTitle title={i18n.t("join_title")} /> <SectionTitle title={i18n.t("join_title")} />
@ -365,20 +426,19 @@ export class Instances extends Component<any, State> {
<div className="flex-none"> <div className="flex-none">
<select <select
className={`${SELECT_CLASSES} mr-2`} className={`${SELECT_CLASSES} mr-2`}
value={this.state.category.name} value={this.state.topic.name}
onChange={linkEvent(this, handleCategoryChange)} onChange={linkEvent(this, handleTopicChange)}
name="category_select" name="topic_select"
> >
<option disabled selected> <option disabled selected>
Category {i18n.t("topic")}
</option> </option>
{CATEGORIES.map(c => ( {TOPICS.map(c => (
<option key={c.name} value={c.name}> <option key={c.name} value={c.name}>
{c.name} {i18n.t(c.name as I18nKeys)}
</option> </option>
))} ))}
</select> </select>
<select <select
value={this.state.language} value={this.state.language}
onChange={linkEvent(this, handleLanguageChange)} onChange={linkEvent(this, handleLanguageChange)}
@ -386,11 +446,11 @@ export class Instances extends Component<any, State> {
> >
<option disabled>Languages</option> <option disabled>Languages</option>
<option key="all" value="all"> <option key="all" value="all">
all {i18n.t("all_languages")}
</option> </option>
{languageList().map((language, i) => ( {LANGUAGES.map((l, i) => (
<option key={i} value={language}> <option key={i} value={l.code}>
{LANGUAGES.find(l => l.code.startsWith(language)).name} {l.name}
</option> </option>
))} ))}
</select> </select>
@ -400,10 +460,10 @@ export class Instances extends Component<any, State> {
className={SELECT_CLASSES} className={SELECT_CLASSES}
onChange={linkEvent(this, handleSortChange)} onChange={linkEvent(this, handleSortChange)}
> >
<option disabled>Sort TODO</option> <option disabled>{i18n.t("sort")}</option>
{SORTS.map(s => ( {SORTS.map(s => (
<option key={s.name} value={s.name}> <option key={s.name} value={s.name}>
{s.name} {i18n.t(s.name as I18nKeys)}
</option> </option>
))} ))}
</select> </select>
@ -415,13 +475,29 @@ export class Instances extends Component<any, State> {
} }
function handleSortChange(i: Instances, event: any) { function handleSortChange(i: Instances, event: any) {
i.setState({ sort: SORTS.find(s => s.name == event.target.value) }); i.setState({
sort: SORTS.find(s => s.name == event.target.value) ?? RANDOM_SORT,
});
i.buildInstanceList();
} }
function handleCategoryChange(i: Instances, event: any) { function handleTopicChange(i: Instances, event: any) {
i.setState({ category: CATEGORIES.find(c => c.name == event.target.value) }); i.setState({
topic: TOPICS.find(c => c.name == event.target.value) ?? All_TOPIC,
});
i.buildInstanceList();
} }
function handleLanguageChange(i: Instances, event: any) { function handleLanguageChange(i: Instances, event: any) {
i.setState({ language: event.target.value }); i.setState({ language: event.target.value });
i.buildInstanceList();
}
function handleSeeAll(i: Instances) {
i.setState({
sort: RANDOM_SORT,
language: "all",
topic: All_TOPIC,
});
i.buildInstanceList();
} }

View file

@ -11,6 +11,7 @@ import {
SupportDonateBlock, SupportDonateBlock,
TEXT_GRADIENT, TEXT_GRADIENT,
} from "./common"; } from "./common";
import { InstancePicker } from "./instance-picker";
const TitleBlock = () => ( const TitleBlock = () => (
<div className="py-16 flex flex-col items-center"> <div className="py-16 flex flex-col items-center">
@ -18,9 +19,9 @@ const TitleBlock = () => (
<p className={`text-6xl font-bold ${TEXT_GRADIENT}`}>Lemmy</p> <p className={`text-6xl font-bold ${TEXT_GRADIENT}`}>Lemmy</p>
<p className="text-3xl font-medium text-center">{i18n.t("lemmy_desc")}</p> <p className="text-3xl font-medium text-center">{i18n.t("lemmy_desc")}</p>
</div> </div>
<div className="flex flex-row justify-around gap-2"> <div className="flex flex-row justify-around gap-4">
<JoinServerButton /> <JoinServerButton />
<RunServerButton /> <SeeAllServersButton />
</div> </div>
</div> </div>
); );
@ -62,18 +63,18 @@ const CarouselBlock = () => (
); );
const JoinServerButton = () => ( const JoinServerButton = () => (
<Link className="btn btn-primary text-white normal-case" to="/instances"> <button
className="btn btn-primary text-white normal-case"
onClick={() => (document.getElementById("picker") as any).showModal()}
>
{i18n.t("join_a_server")} {i18n.t("join_a_server")}
</Link> </button>
); );
const RunServerButton = () => ( const SeeAllServersButton = () => (
<a <Link to="/instances" className="btn btn-secondary text-white normal-case">
class="btn btn-secondary text-white normal-case" {i18n.t("see_all_servers")}
href={`/docs/administration/administration.html`} </Link>
>
{i18n.t("run_a_server")}
</a>
); );
const FollowCommunitiesBlock = () => ( const FollowCommunitiesBlock = () => (
@ -86,7 +87,7 @@ const FollowCommunitiesBlock = () => (
> >
#<span className={TEXT_GRADIENT}>#</span> #<span className={TEXT_GRADIENT}>#</span>
</T> </T>
<p class="text-sm text-gray-300 text-center mb-6"> <p className="text-sm text-gray-300 text-center mb-6">
{i18n.t("lemmy_long_desc")} {i18n.t("lemmy_long_desc")}
</p> </p>
<JoinServerButton /> <JoinServerButton />
@ -220,12 +221,12 @@ const DiscussionPlatformBlock = () => (
# #
</a> </a>
</T> </T>
<Link <a
className="btn btn-primary bg-white text-primary normal-case" className="btn btn-primary bg-white text-primary normal-case"
to="/instances" href={`/docs/administration/administration.html`}
> >
{i18n.t("join_a_server")} {i18n.t("run_a_server")}
</Link> </a>
</div> </div>
</div> </div>
</div> </div>
@ -272,7 +273,7 @@ const MoreFeaturesBlock = () => (
</div> </div>
} }
text={ text={
<Link class="link link-primary" to="/apps"> <Link className="link link-primary" to="/apps">
{i18n.t("mobile_apps_for_ios_and_android")} {i18n.t("mobile_apps_for_ios_and_android")}
</Link> </Link>
} }
@ -293,7 +294,7 @@ const MoreFeaturesBlock = () => (
} }
text={ text={
<T i18nKey="full_vote_scores"> <T i18nKey="full_vote_scores">
#<code class="text-primary">#</code># #<code className="text-primary">#</code>#
</T> </T>
} }
/> />
@ -309,7 +310,7 @@ const MoreFeaturesBlock = () => (
icons={<div>:</div>} icons={<div>:</div>}
text={ text={
<T i18nKey="emojis_autocomplete"> <T i18nKey="emojis_autocomplete">
#<code class="text-primary">#</code> #<code className="text-primary">#</code>
</T> </T>
} }
/> />
@ -321,8 +322,8 @@ const MoreFeaturesBlock = () => (
} }
text={ text={
<T i18nKey="user_tagging"> <T i18nKey="user_tagging">
#<code class="text-primary">#</code> #<code className="text-primary">#</code>
<code class="text-primary">#</code> <code className="text-primary">#</code>
</T> </T>
} }
/> />
@ -368,11 +369,11 @@ const MoreFeaturesBlock = () => (
} }
text={ text={
<T i18nKey="rss_feeds"> <T i18nKey="rss_feeds">
#<code class="text-primary">#</code> #<code className="text-primary">#</code>
<code class="text-primary">#</code> <code className="text-primary">#</code>
<code class="text-primary">#</code> <code className="text-primary">#</code>
<code class="text-primary">#</code> <code className="text-primary">#</code>
<code class="text-primary">#</code> <code className="text-primary">#</code>
</T> </T>
} }
/> />
@ -428,6 +429,7 @@ export class Main extends Component<any, any> {
const title = i18n.t("lemmy_title"); const title = i18n.t("lemmy_title");
return ( return (
<div> <div>
<InstancePicker />
<Helmet title={title}> <Helmet title={title}>
<meta property={"title"} content={title} /> <meta property={"title"} content={title} />
</Helmet> </Helmet>

View file

@ -4,7 +4,6 @@ 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 } from "./common"; import { SELECT_CLASSES } from "./common";
import { languageList } from "../utils";
const NavLink = ({ content }) => <li className="text-gray-400">{content}</li>; const NavLink = ({ content }) => <li className="text-gray-400">{content}</li>;
@ -52,13 +51,13 @@ export const Navbar = ({ footer = false }) => (
onChange={linkEvent(this, handleLanguageChange)} onChange={linkEvent(this, handleLanguageChange)}
className={SELECT_CLASSES} className={SELECT_CLASSES}
> >
{languageList().map((language, i) => ( {LANGUAGES.map((l, i) => (
<option <option
key={i} key={i}
value={language} value={l.code}
selected={i18n.language.startsWith(language)} selected={i18n.language.startsWith(l.code)}
> >
{LANGUAGES.find(l => l.code.startsWith(language)).name} {l.name}
</option> </option>
))} ))}
</select> </select>

View file

@ -21,7 +21,7 @@ export class NewsItem extends Component<any, any> {
get markdown(): string { get markdown(): string {
let title = decodeURIComponent(this.props.match.params.title); let title = decodeURIComponent(this.props.match.params.title);
title = title.replace(/_/g, " "); title = title.replace(/_/g, " ");
return news_md.find(v => v.title == title).markdown; return news_md.find(v => v.title == title)?.markdown ?? "";
} }
render() { render() {

View file

@ -1,5 +1,4 @@
import markdown_it from "markdown-it"; import markdown_it from "markdown-it";
import { i18n } from "./i18next";
let SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", { let SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1, maximumFractionDigits: 1,
@ -26,6 +25,20 @@ export function mdToHtml(text: string) {
return { __html: md.render(text) }; return { __html: md.render(text) };
} }
export function languageList() { export function getQueryParams<T extends Record<string, any>>(processors: {
return Object.keys(i18n.services.resourceStore.data).sort(); [K in keyof T]: (param: string) => T[K];
}): T {
if (isBrowser()) {
const searchParams = new URLSearchParams(window.location.search);
return Array.from(Object.entries(processors)).reduce(
(acc, [key, process]) => ({
...acc,
[key]: process(searchParams.get(key)),
}),
{} as T,
);
}
return {} as T;
} }

View file

@ -3,6 +3,7 @@
"pretty": true, "pretty": true,
"target": "esnext", "target": "esnext",
"module": "esnext", "module": "esnext",
"strictNullChecks": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"sourceMap": true, "sourceMap": true,