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>
);
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}
path={path}
exact={exact}
render={props => <C {...props} {...rest} />}
render={props => C && <C {...props} {...rest} />}
/>
))}
<Route render={props => <NoMatch {...props} />} />

View file

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

View file

@ -3,16 +3,17 @@ import { Helmet } from "inferno-helmet";
import { i18n, LANGUAGES } from "../i18next";
import { T } from "inferno-i18next";
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 {
INSTANCE_HELPERS,
Category,
Topic,
RECOMMENDED_INSTANCES,
All_CATEGORY,
CATEGORIES,
All_TOPIC,
TOPICS,
} from "./instances-definitions";
import { Icon } from "./icon";
import { I18nKeys } from "i18next";
const TitleBlock = () => (
<div className="flex flex-col items-center pt-16 mb-16">
@ -64,9 +65,6 @@ interface InstanceCardGridProps {
instances: any[];
}
// 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 => (
@ -282,23 +280,67 @@ function sortActive(instances: any[]): any[] {
}
interface State {
instances: any[];
sort: Sort;
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 = {
sort: SORTS[0],
language: i18n.language.split("-")[0],
category: All_CATEGORY,
instances: [],
sort: RANDOM_SORT,
language: "all",
topic: All_TOPIC,
scroll: false,
};
constructor(props: any, context: any) {
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;
const recommended = RECOMMENDED_INSTANCES;
@ -312,26 +354,26 @@ export class Instances extends Component<any, State> {
);
}
// Category filter
if (this.state.category !== All_CATEGORY) {
const categoryRecs = recommended.filter(r =>
r.categories.includes(this.state.category),
// Topic filter
if (this.state.topic !== All_TOPIC) {
const topicRecs = recommended.filter(r =>
r.topics.includes(this.state.topic),
);
instances = instances.filter(i =>
categoryRecs.map(c => c.domain).includes(i.domain),
topicRecs.map(c => c.domain).includes(i.domain),
);
}
// Sort
if (this.state.sort == SORTS[0]) {
if (this.state.sort == RANDOM_SORT) {
instances = sortRandom(instances);
} else if (this.state.sort == SORTS[1]) {
} else if (this.state.sort == MOST_ACTIVE_SORT) {
instances = sortActive(instances);
} else {
instances = sortActive(instances).reverse();
}
return instances;
this.setState({ instances });
}
render() {
@ -345,18 +387,37 @@ export class Instances extends Component<any, State> {
<TitleBlock />
<ComparisonBlock />
{this.filterAndTitleBlock()}
<InstanceCardGrid
title={i18n.t("popular_instances")}
instances={this.buildInstanceList()}
/>
<div className="mt-4">
{this.state.instances.length > 0 ? (
<InstanceCardGrid
title={i18n.t("popular_instances")}
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>
);
}
// TODO i18n these
filterAndTitleBlock() {
return (
<div className="my-16">
<div id="search" className="mt-16">
<div className="flex flex-row flex-wrap gap-4">
<div className="flex-none">
<SectionTitle title={i18n.t("join_title")} />
@ -365,20 +426,19 @@ export class Instances extends Component<any, State> {
<div className="flex-none">
<select
className={`${SELECT_CLASSES} mr-2`}
value={this.state.category.name}
onChange={linkEvent(this, handleCategoryChange)}
name="category_select"
value={this.state.topic.name}
onChange={linkEvent(this, handleTopicChange)}
name="topic_select"
>
<option disabled selected>
Category
{i18n.t("topic")}
</option>
{CATEGORIES.map(c => (
{TOPICS.map(c => (
<option key={c.name} value={c.name}>
{c.name}
{i18n.t(c.name as I18nKeys)}
</option>
))}
</select>
<select
value={this.state.language}
onChange={linkEvent(this, handleLanguageChange)}
@ -386,11 +446,11 @@ export class Instances extends Component<any, State> {
>
<option disabled>Languages</option>
<option key="all" value="all">
all
{i18n.t("all_languages")}
</option>
{languageList().map((language, i) => (
<option key={i} value={language}>
{LANGUAGES.find(l => l.code.startsWith(language)).name}
{LANGUAGES.map((l, i) => (
<option key={i} value={l.code}>
{l.name}
</option>
))}
</select>
@ -400,10 +460,10 @@ export class Instances extends Component<any, State> {
className={SELECT_CLASSES}
onChange={linkEvent(this, handleSortChange)}
>
<option disabled>Sort TODO</option>
<option disabled>{i18n.t("sort")}</option>
{SORTS.map(s => (
<option key={s.name} value={s.name}>
{s.name}
{i18n.t(s.name as I18nKeys)}
</option>
))}
</select>
@ -415,13 +475,29 @@ export class Instances extends Component<any, State> {
}
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) {
i.setState({ category: CATEGORIES.find(c => c.name == event.target.value) });
function handleTopicChange(i: Instances, event: any) {
i.setState({
topic: TOPICS.find(c => c.name == event.target.value) ?? All_TOPIC,
});
i.buildInstanceList();
}
function handleLanguageChange(i: Instances, event: any) {
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,
TEXT_GRADIENT,
} from "./common";
import { InstancePicker } from "./instance-picker";
const TitleBlock = () => (
<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-3xl font-medium text-center">{i18n.t("lemmy_desc")}</p>
</div>
<div className="flex flex-row justify-around gap-2">
<div className="flex flex-row justify-around gap-4">
<JoinServerButton />
<RunServerButton />
<SeeAllServersButton />
</div>
</div>
);
@ -62,18 +63,18 @@ const CarouselBlock = () => (
);
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")}
</Link>
</button>
);
const RunServerButton = () => (
<a
class="btn btn-secondary text-white normal-case"
href={`/docs/administration/administration.html`}
>
{i18n.t("run_a_server")}
</a>
const SeeAllServersButton = () => (
<Link to="/instances" className="btn btn-secondary text-white normal-case">
{i18n.t("see_all_servers")}
</Link>
);
const FollowCommunitiesBlock = () => (
@ -86,7 +87,7 @@ const FollowCommunitiesBlock = () => (
>
#<span className={TEXT_GRADIENT}>#</span>
</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")}
</p>
<JoinServerButton />
@ -220,12 +221,12 @@ const DiscussionPlatformBlock = () => (
#
</a>
</T>
<Link
<a
className="btn btn-primary bg-white text-primary normal-case"
to="/instances"
href={`/docs/administration/administration.html`}
>
{i18n.t("join_a_server")}
</Link>
{i18n.t("run_a_server")}
</a>
</div>
</div>
</div>
@ -272,7 +273,7 @@ const MoreFeaturesBlock = () => (
</div>
}
text={
<Link class="link link-primary" to="/apps">
<Link className="link link-primary" to="/apps">
{i18n.t("mobile_apps_for_ios_and_android")}
</Link>
}
@ -293,7 +294,7 @@ const MoreFeaturesBlock = () => (
}
text={
<T i18nKey="full_vote_scores">
#<code class="text-primary">#</code>#
#<code className="text-primary">#</code>#
</T>
}
/>
@ -309,7 +310,7 @@ const MoreFeaturesBlock = () => (
icons={<div>:</div>}
text={
<T i18nKey="emojis_autocomplete">
#<code class="text-primary">#</code>
#<code className="text-primary">#</code>
</T>
}
/>
@ -321,8 +322,8 @@ const MoreFeaturesBlock = () => (
}
text={
<T i18nKey="user_tagging">
#<code class="text-primary">#</code>
<code class="text-primary">#</code>
#<code className="text-primary">#</code>
<code className="text-primary">#</code>
</T>
}
/>
@ -368,11 +369,11 @@ const MoreFeaturesBlock = () => (
}
text={
<T i18nKey="rss_feeds">
#<code class="text-primary">#</code>
<code class="text-primary">#</code>
<code class="text-primary">#</code>
<code class="text-primary">#</code>
<code class="text-primary">#</code>
#<code className="text-primary">#</code>
<code className="text-primary">#</code>
<code className="text-primary">#</code>
<code className="text-primary">#</code>
<code className="text-primary">#</code>
</T>
}
/>
@ -428,6 +429,7 @@ export class Main extends Component<any, any> {
const title = i18n.t("lemmy_title");
return (
<div>
<InstancePicker />
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>

View file

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

View file

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

View file

@ -1,5 +1,4 @@
import markdown_it from "markdown-it";
import { i18n } from "./i18next";
let SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1,
@ -26,6 +25,20 @@ export function mdToHtml(text: string) {
return { __html: md.render(text) };
}
export function languageList() {
return Object.keys(i18n.services.resourceStore.data).sort();
export function getQueryParams<T extends Record<string, any>>(processors: {
[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,
"target": "esnext",
"module": "esnext",
"strictNullChecks": true,
"allowSyntheticDefaultImports": true,
"preserveConstEnums": true,
"sourceMap": true,