Building a Debate App: Part 2

By akohad Apr26,2023

[ad_1]

Decentralization / Social media

Implementing the debate list

npm install -g @ionic/cli
ionic start debate tabs --type=react
cd debate
ionic serve

Home page tab

    return <IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route exact path="/home">
<HomePage />
</Route>
...
<Route exact path="/">
<Redirect to="/home" />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/home">
<IonIcon icon={homeSharp} />
<IonLabel>Home</IonLabel>
</IonTabButton>
...
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
import { ellipse, homeSharp, square } from 'ionicons/icons';
import HomePage from './pages/HomePage';
import './HomePage.css';

const HomePage: React.FC = () => {
...
};

export default HomePage;

Header component

import { IonAvatar, IonHeader, IonIcon, IonItem, IonToolbar } from '@ionic/react';
import './DebateHeader.css';
import { chatbubbles } from 'ionicons/icons';

const DebateHeader: React.FC = () => {
return (
<IonHeader>
<IonToolbar>
<IonItem>
<IonAvatar slot="start"><img src="https://ionicframework.com/docs/img/demos/avatar.svg"/></IonAvatar>
<IonIcon className="app-icon" icon={chatbubbles}></IonIcon>
</IonItem>
</IonToolbar>
</IonHeader>
);
};

export default DebateHeader;

.app-icon {
position: absolute;
top: 50%;
left: 50%;
margin: -12px -42px;
}
<DebateHeader />
import DebateHeader from '../components/DebateHeader';

Debate card

npm install react-player
import { IonAvatar, IonBadge, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonIcon, IonItem, IonLabel, IonList, IonText, IonToolbar } from '@ionic/react';
import './DebateCard.css';
import ReactPlayer from 'react-player';
import { heartSharp, peopleSharp, starSharp, thumbsDownSharp, thumbsUpSharp } from 'ionicons/icons';

interface ContainerProps {
title: string;
description: string;
username: string;
url: string;
}

const DebateCard: React.FC<ContainerProps> = ({ title, description, username, url }) => {
return (
<IonCard>
<IonCardHeader>
<IonItem className="head-item" lines="none">
<IonAvatar slot="start"><img src="https://ionicframework.com/docs/img/demos/avatar.svg" /></IonAvatar>
<IonLabel color="medium"><strong>@{username}</strong> - Just now</IonLabel>
</IonItem>
<IonCardTitle>{title}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<p>{description}</p>
</IonCardContent>
{url && ReactPlayer.canPlay(url) ? <div className='player-wrapper'>
<ReactPlayer className='react-player' url={url} width='100%' height='100%' />
</div> : null}
<IonToolbar>
<IonItem className="counts">
<IonItem>
<IonIcon size="small" icon={thumbsUpSharp} />
<IonBadge className="count">11</IonBadge>
</IonItem>
<IonItem>
<IonIcon size="small" icon={thumbsDownSharp} />
<IonBadge className="count">11</IonBadge>
</IonItem>
<IonItem>
<IonIcon size="small" icon={starSharp} />
<IonBadge className="count">11</IonBadge>
</IonItem>
<IonItem>
<IonIcon size="small" icon={heartSharp} />
<IonBadge className="count">11</IonBadge>
</IonItem>
<IonItem>
<IonIcon size="small" icon={peopleSharp} />
<IonBadge className="count">1</IonBadge>
</IonItem>
</IonItem>
</IonToolbar>
</IonCard>
);
};

export default DebateCard;

.head-item {
--padding-start: 0;
}

.player-wrapper {
position: relative;
padding-top: 56.25%
}

.react-player {
position: absolute;
top: 0;
left: 0;
}

.counts {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.count {
--background: rgba(0, 0, 0, 0);
--color: var(--ion-color-medium);
}

            <IonContent fullscreen>
<DebateCard username="xyzabc" title="My debate" description="All about my debate" url="https://www.youtube.com/embed/jaJuwKCmJbY" />
</IonContent>
import DebateCard from '../components/DebateCard';

Floating action button

            <IonFab slot="fixed" vertical="bottom" horizontal="end">
<IonFabButton>
<IonIcon icon={add}/>
</IonFabButton>
</IonFab>
import { IonContent, IonFab, IonFabButton, IonIcon, IonPage } from '@ionic/react';
import { add } from 'ionicons/icons';

Add-debate modal

import { IonAvatar, IonButton, IonButtons, IonContent, IonDatetime, IonHeader, IonIcon, IonInput, IonItem, IonItemDivider, IonLabel, IonModal, IonTextarea, IonToolbar } from '@ionic/react';
import { add, peopleSharp, remove } from 'ionicons/icons';
import './DebateAddModal.css';

interface ContainerProps {
isOpen: boolean;
setIsOpen: (x: boolean) => void;
}

const DebateAddModal: React.FC<ContainerProps> = ({ isOpen, setIsOpen }) => { return (
<IonModal isOpen={isOpen}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton onClick={() => setIsOpen(false)}>Cancel</IonButton>
</IonButtons>
<IonButtons slot="end">
<IonButton>
<IonIcon icon={add}/>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonItem>
<IonAvatar slot="start"><img src="https://ionicframework.com/docs/img/demos/avatar.svg"/></IonAvatar>
<IonInput placeholder="What say you?"/>
</IonItem>
<IonItem>
<IonTextarea autoGrow={true} placeholder="Tell me more..."/>
</IonItem>

<IonItemDivider>
<IonLabel>Voting groups</IonLabel>
</IonItemDivider>

<IonItem>
<IonIcon slot="start" icon={peopleSharp} />
<IonInput color="medium" value="Default" readonly={true}/>
<div style={{ 'width': '50px' }}>
<IonInput type="number" color="medium" value="75" readonly={true}/>
</div>
<IonLabel color="medium">%</IonLabel>
<IonButton slot="end">
<IonIcon icon={add}/>
</IonButton>
</IonItem>
<IonItem>
<IonIcon slot="start" icon={peopleSharp} />
<IonInput color="medium" placeholder="Group name" value="My group"/>
<div style={{ 'width': '50px' }}>
<IonInput type="number" color="medium" value="25"/>
</div>
<IonLabel color="medium">%</IonLabel>
<IonButton slot="end">
<IonIcon icon={remove} />
</IonButton>
</IonItem>

<IonItemDivider>
<IonLabel>Schedule</IonLabel>
</IonItemDivider>

<IonDatetime presentation="date-time" preferWheel={true} size="cover">
<span slot="title">Start time</span>
</IonDatetime>
<IonDatetime presentation="date-time" preferWheel={true} size="cover">
<span slot="title">End time</span>
</IonDatetime>
</IonContent>
</IonModal>
);
};

export default DebateAddModal;

            <IonFab slot="fixed" vertical="bottom" horizontal="end">
<IonFabButton onClick={() => setIsOpen(true)}>
<IonIcon icon={add}/>
</IonFabButton>
</IonFab>
<DebateAddModal isOpen={isOpen} setIsOpen={setIsOpen} />
    const [isOpen, setIsOpen] = useState(false);
import DebateAddModal from '../components/DebateAddModal';
import { useState } from 'react';

Add or remove groups

const [groups, setGroups] = useState(new Array<IGroup>());
export interface IGroup {
name: string;
percent: number;
}
import { IGroup } from '../AppData';
import { useState } from 'react';
    const totalPercent = groups.map(g => g.percent).reduce((p, c) => p + c, 0);
                {totalPercent < 100 ? <IonItem>
<IonIcon slot="start" icon={peopleSharp} />
<IonInput color="medium" value="Default" readonly={true} />
<div style={{ 'width': '50px' }}>
<IonInput type="number" color="medium" value={100 - totalPercent} readonly={true} />
</div>
<IonLabel color="medium">%</IonLabel>
<IonButton slot="end" onClick={() => setGroups([...groups, { name: '', percent: 0 }])}>
<IonIcon icon={add} />
</IonButton>
</IonItem> : null}
                {groups.map((g, i) => <IonItem>
<IonIcon slot="start" icon={peopleSharp} />
<IonInput color="medium" placeholder="Group name" value={g.name}/>
<div style={{ 'width': '50px' }}>
<IonInput type="number" color="medium" value={g.percent}/>
</div>
<IonLabel color="medium">%</IonLabel>
<IonButton slot="end" onClick={() => setGroups([...groups.slice(0, i), ...groups.slice(i + 1)])}>
<IonIcon icon={remove} />
</IonButton>
</IonItem>)}
    const updateName = (i: number, value: string | null | undefined) => {
if (!value)
return;

setGroups([...groups.slice(0, i), { name: value, percent: groups[i].percent }, ...groups.slice(i + 1)])
};

const updatePercent = (i: number, value: string | null | undefined) => {
if (!value)
return;

let newPercent = parseFloat(value);
if (Number.isNaN(newPercent))
return;

// Limit total % to 100
const oldPercent = groups[i].percent;
if (totalPercent - oldPercent + newPercent > 100)
newPercent = 100 - (totalPercent - oldPercent);

setGroups([...groups.slice(0, i), { name: groups[i].name, percent: newPercent }, ...groups.slice(i + 1)])
};

                    <IonInput color="medium" placeholder="Group name" value={g.name} onIonChange={e => updateName(i, e.detail.value)} />
<div style={{ 'width': '50px' }}>
<IonInput type="number" color="medium" value={g.percent} onIonChange={e => updatePercent(i, e.detail.value)} />
</div>
    const today = new Date();
const addDay = (d: Date) => {
const d_next = new Date();
d_next.setDate(d.getDate() + 1);
return d_next;
}

const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [startTime, setStartTime] = useState(today);
const [endTime, setEndTime] = useState(addDay(today));

    const updateTitle = (value: string | null | undefined) => {
if (!value && value != '')
return;

setTitle(value);
};

const updateDescription = (value: string | null | undefined) => {
if (!value && value != '')
return;

setDescription(value);
};

const updateStartTime = (value: string | string[] | null | undefined) => {
const s = value as string;
if (!s && s != '')
return;

setStartTime(new Date(s));
};

const updateEndTime = (value: string | string[] | null | undefined) => {
const s = value as string;
if (!s && s != '')
return;

setEndTime(new Date(s));
};

                    ...
<IonInput placeholder="What say you?" value={title} onIonChange={e => updateTitle(e.detail.value)} />
...
<IonTextarea autoGrow={true} placeholder="Tell me more..." value={description} onIonChange={e => updateDescription(e.detail.value)} />
...
<IonDatetime presentation="date-time" preferWheel={true} size="cover" min={today.toISOString()} value={startTime.toISOString()} onIonChange={e => updateStartTime(e.detail.value)}>
...
<IonDatetime presentation="date-time" preferWheel={true} size="cover" min={addDay(startTime).toISOString()} value={endTime.toISOString()} onIonChange={e => updateEndTime(e.detail.value)}>
...
                        <IonButton disabled={title.length == 0}>
<IonIcon icon={add}/>
</IonButton>

Installing IPFS and Bonono for React

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/index.min.js"></script>
npm install bonono-react

Adding the BononoDb component

import { BononoDb, IDbClient } from 'bonono-react';
        <BononoDb address="/dns4/nyk.webrtc-star.bonono.org/tcp/443/wss/p2p-webrtc-star/" onDbClient={e => loadDb(e.detail)} />

Loading the database

    const loadDb = async (dbClient: IDbClient | null) => {
if (!dbClient)
return;

if (!await dbClient.connect())
return;

const db = await dbClient.db("debate-app");
if (!db)
return;

appData.init(db);
}

Loading collections

    const [appData] = useState(new AppData());
import { AppData } from './AppData';
import { useState } from 'react';
export class AppData {
private _debatesCollection: IDbCollection | null = null;
private _debateCallbacks: (() => void)[] = [];
private _debates: IDebate[] = [];

private _notifyDebatesUpdated() {
if (!this._debatesCollection)
return;
this._debates = Array.from(this._debatesCollection.all).map(kvp => kvp[1]);
for (const cb of this._debateCallbacks)
cb();
}

async init(db: IDb) {
this._debatesCollection = await db.collection("debate", { publicAccess: AccessRights.ReadWrite, conflictResolution: ConflictResolution.FirstWriteWins });
this._debatesCollection.onUpdated(() => this._notifyDebatesUpdated());
this._notifyDebatesUpdated()
}

debates(): IDebate[] {
return this._debates;
}

onDebatesUpdated(callback: () => void) {
this._debateCallbacks.push(callback);
}

addDebate(debate: IDebate) {
this._debatesCollection?.insertOne(debate);
}
}

import { AccessRights, ConflictResolution, IDb, IDbCollection } from "bonono-react";
export interface IDbEntry {
_id: string;
_clock: number;
_identity: { publicKey: string };
}
export const dbEntryDefaults = { _id: '', _clock: 0, _identity: { publicKey: '' } };

export interface IGroup {
name: string;
percent: number;
}

export interface IDebate extends IDbEntry {
title: string;
description: string;
startTime: string;
endTime: string;
groups: IGroup[];
}

Passing collections around

                    <Route exact path="/home">
<HomePage appData={appData} />
</Route>
interface ContainerProps {
appData: AppData;
}

const HomePage: React.FC<ContainerProps> = ({ appData }) => {
...
};

import { AppData } from '../AppData';
            <DebateAddModal appData={appData} isOpen={isOpen} setIsOpen={setIsOpen} />
interface ContainerProps {
appData: AppData;
isOpen: boolean;
setIsOpen: (x: boolean) => void;
}

const DebateAddModal: React.FC<ContainerProps> = ({ appData, isOpen, setIsOpen }) => {
...
};

import { AppData, IGroup } from '../AppData';

Adding a debate to the collection

    const addDebate = () => {
const debate: IDebate = {
...dbEntryDefaults,
title,
description,
groups,
startTime: startTime.toISOString(),
endTime: endTime.toISOString()
};
appData.addDebate(debate);
setIsOpen(false);
}
import { AppData, dbEntryDefaults, IDebate, IGroup } from '../AppData';
                        <IonButton disabled={title.length == 0} onClick={() => addDebate()}>
<IonIcon icon={add} />
</IonButton>

Reading debates from the collection

    const [debates, setDebates] = useState(new Array<IDebate>());
import { AppData, IDebate } from '../AppData';
    appData.onDebatesUpdated(() => { setDebates(appData.debates()); });
                {debates.map(d => <DebateCard username={d._identity.publicKey.slice(-8)} title={d.title} description={d.description} url={findUrl(d.description)} />)}
    const findUrl = (description: string) => {
const urlRegex = /(https?:\/\/[^ ]*)/g;
const match = description.match(urlRegex);
return match ? match[match.length - 1] : '';
}

Summary

[ad_2]

Source link

By akohad

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *