Skip to content

Commit

Permalink
improve UI
Browse files Browse the repository at this point in the history
  • Loading branch information
adbenitez committed Aug 28, 2023
1 parent ddb5b58 commit a130023
Show file tree
Hide file tree
Showing 10 changed files with 438 additions and 85 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@ionic/react-router": "^7.0.0",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"fuse.js": "^6.6.2",
"ionicons": "^7.1.2",
"preact": "^10.17.0",
"react-dom": "^18.2.0",
Expand Down
8 changes: 8 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/src/components/BotItem.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.botChip {
text-decoration: none;
}
47 changes: 47 additions & 0 deletions frontend/src/components/BotItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
IonItem,
IonLabel,
IonChip,
IonBadge,
IonIcon,
IonAvatar,
} from "@ionic/react";
import { languageOutline, openOutline } from "ionicons/icons";

import { Bot } from "../store";
import "./BotItem.css";

export default function BotItem({ bot }: { bot: Bot }) {
return (
<IonItem>
<IonLabel>
<a
className="botChip"
target="_blank"
rel="noopener noreferrer"
href={bot.url}
>
<IonChip>
<IonAvatar>
<img src="icon.png" />
</IonAvatar>
<IonLabel>{bot.addr} </IonLabel>
<IonIcon icon={openOutline} />
</IonChip>
</a>
<br />
<IonBadge color="light">
<IonIcon icon={languageOutline} /> {bot.lang.label}
</IonBadge>
<p className="ion-text-wrap">{bot.description}</p>
<p>
<strong>Admin: </strong>
<a target="_blank" rel="noopener noreferrer" href={bot.admin.url}>
{bot.admin.name}
<IonIcon icon={openOutline} />
</a>
</p>
</IonLabel>
</IonItem>
);
}
5 changes: 5 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { init } from "./store";
import { setupIonicReact } from "@ionic/react";

setupIonicReact({
mode: "ios",
});

const container = document.getElementById("root");
const root = createRoot(container!);
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/pages/Home.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@
width: 100px;
height: 100px;
}

#footer {
text-align: center;
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
}
118 changes: 72 additions & 46 deletions frontend/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,98 @@
import {
IonContent,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonItem,
IonLabel,
IonList,
IonChip,
IonBadge,
IonIcon,
IonSpinner,
IonSearchbar,
IonToast,
IonProgressBar,
} from "@ionic/react";
import { languageOutline, openOutline } from "ionicons/icons";
import { useStore } from "../store";
import Fuse from "fuse.js";
import { create } from "zustand";
import { useState } from "react";
import { warningOutline } from "ionicons/icons";

import { useStore, Bot } from "../store";
import BotItem from "../components/BotItem";
import "./Home.css";

const fuseOptions = {
keys: ["addr", "description", "admin.name", "lang.label"],
threshold: 0.4,
};

interface HomeState {
query: string;
results: Bot[];
}

const homeStore = create<HomeState>()((set) => ({
query: "",
results: [],
}));

const Home: React.FC = () => {
const state = useStore();
const data = state.data || { bots: [], admins: {}, langs: {} };
const query = homeStore((state) => state.query);
let results = homeStore((state) => state.results);
if (!query) {
results = state.bots;
}
const fuse = new Fuse(state.bots, fuseOptions);
const handleInput = (ev: Event) => {
const target = ev.target as HTMLIonSearchbarElement;
const query = target ? target.value!.toLowerCase() : "";
if (query) {
homeStore.setState({
query: query,
results: fuse.search(query).map((result) => result.item),
});
} else {
homeStore.setState({ query: query });
}
};

return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>
Public Bots
{state.lastSync && (
<IonChip>{state.lastSync.toLocaleString()}</IonChip>
)}
</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
{state.syncing && state.lastUpdated && (
<IonProgressBar type="indeterminate"></IonProgressBar>
)}
{state.bots.length > 0 && (
<>
<br />
<IonSearchbar
debounce={200}
onIonInput={(ev) => handleInput(ev)}
placeholder={"Search among " + state.bots.length + " bots"}
></IonSearchbar>
</>
)}
{state.lastUpdated ? (
<IonList>
{data.bots.map((bot) => (
<IonItem>
<IonLabel>
<h2>
<a target="_blank" rel="noopener noreferrer" href={bot.url}>
{bot.addr}
<IonIcon icon={openOutline} />
</a>
</h2>
<IonBadge color="light">
<IonIcon icon={languageOutline} /> {data.langs[bot.lang]}
</IonBadge>
<p className="ion-text-wrap">{bot.description}</p>
<p>
<strong>Admin: </strong>
<a
target="_blank"
rel="noopener noreferrer"
href={data.admins[bot.admin].url}
>
{bot.admin}
<IonIcon icon={openOutline} />
</a>
</p>
</IonLabel>
</IonItem>
{results.map((bot) => (
<BotItem bot={bot} />
))}
</IonList>
) : (
<div id="loading">
<IonSpinner name="dots"></IonSpinner>
</div>
)}
{state.lastSync && (
<p id="footer">Last updated: {state.lastSync.toLocaleString()}</p>
)}
{state.error && (
<IonToast
isOpen={true}
message={"[" + state.error.code + "] " + state.error.message}
icon={warningOutline}
color="danger"
onDidDismiss={() => useStore.setState({ error: undefined })}
duration={5000}
></IonToast>
)}
</IonContent>
</IonPage>
);
Expand Down
71 changes: 46 additions & 25 deletions frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const api = (() => {
return {
sync: () => {
localStorage.setItem(lastSyncReqKey, new Date().toString());
useStore.setState({ syncing: true });
window.webxdc.sendUpdate(
{ payload: { id: "sync", method: "Sync", params: [null] } },
"",
Expand All @@ -14,50 +15,66 @@ const api = (() => {
const lastSyncKey = "LastSyncKey";
const lastSyncReqKey = "LastSyncReqKey";
const lastUpdatedKey = "LastUpdatedKey";
const dataKey = "DataKey";
const botsKey = "BotsKey";
const maxSerialKey = "MaxSerialKey";

type Response = {
id: string;
result?: any;
error?: { code: number; message: string; data: any };
error?: Error;
};
type Admin = { url: string };
type Bot = {
type Error = { code: number; message: string; data: any };
type Admin = { name: string; url: string };
type Lang = { label: string; code: string };
export type Bot = {
addr: string;
url: string;
description: string;
lang: string;
admin: string;
lang: Lang;
admin: Admin;
};
interface State {
lastSync?: Date;
lastUpdated?: string;
data?: {
bots: Bot[];
admins: { [key: string]: Admin };
langs: { [key: string]: string };
};
bots: Bot[];
error?: Error;
syncing: boolean;
applyWebxdcUpdate: (update: Response) => void;
}

export const useStore = create<State>()((set) => ({
syncing: false,
bots: [],
applyWebxdcUpdate: (update: Response) =>
set((state) => {
state = { ...state, syncing: false };
if (update.error) {
return; // TODO: display error?
return {
...state,
error: update.error,
};
}
const result = update.result;
if (result) {
localStorage.setItem(lastSyncKey, new Date().toString());
localStorage.setItem(lastUpdatedKey, result.lastUpdated);
localStorage.setItem(dataKey, JSON.stringify(result.data));
const newState = {
const data = result.data;
data.bots.map((bot: any) => {
bot.admin = { ...data.admins[bot.admin], name: bot.admin };
bot.lang = { code: bot.lang, label: data.langs[bot.lang] };
});
data.bots.sort((a: Bot, b: Bot) => {
if (a.addr < b.addr) {
return -1;
}
return 1;
});
localStorage.setItem(botsKey, JSON.stringify(data.bots));
return {
...state,
lastUpdated: result.lastUpdated,
data: result.data,
bots: data.bots,
};
return result;
}
return state;
}),
Expand All @@ -80,20 +97,24 @@ export async function init() {
const lastUpdated = localStorage.getItem(lastUpdatedKey);
if (lastUpdated) {
const lastSync = new Date(localStorage.getItem(lastSyncKey) || "");
const data = JSON.parse(localStorage.getItem(dataKey) || "");
const bots = JSON.parse(localStorage.getItem(botsKey) || "");
useStore.setState({
...useStore.getState(),
lastSync: lastSync,
lastUpdated: lastUpdated,
data: data,
bots: bots,
});
}

const lastSyncReq = localStorage.getItem(lastSyncReqKey) || "";
if (
!lastSyncReq ||
new Date().getTime() - new Date(lastSyncReq).getTime() > 1000 * 60 * 10
) {
api.sync();
}
setInterval(() => {
const lastSyncReq = localStorage.getItem(lastSyncReqKey) || "";
const lastUpdated = localStorage.getItem(lastUpdatedKey);
if (
!lastSyncReq ||
new Date().getTime() - new Date(lastSyncReq).getTime() >
1000 * 60 * (lastUpdated ? 10 : 1)
) {
api.sync();
}
}, 5000);
}
Loading

0 comments on commit a130023

Please sign in to comment.