more fn comps

This commit is contained in:
Martti Malmi 2023-08-19 18:42:44 +03:00
parent 209f7a3fad
commit 3842ba5e9d
8 changed files with 441 additions and 457 deletions

View File

@ -18,6 +18,7 @@ module.exports = {
plugins: ['simple-import-sort', '@typescript-eslint'], plugins: ['simple-import-sort', '@typescript-eslint'],
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'simple-import-sort/imports': [ 'simple-import-sort/imports': [
'error', 'error',
{ {

View File

@ -1,4 +1,5 @@
import Component from '../../BaseComponent'; import { useEffect, useState } from 'react';
import Key from '../../nostr/Key'; import Key from '../../nostr/Key';
import SocialNetwork from '../../nostr/SocialNetwork'; import SocialNetwork from '../../nostr/SocialNetwork';
import { translate as t } from '../../translations/Translation.mjs'; import { translate as t } from '../../translations/Translation.mjs';
@ -11,77 +12,61 @@ type Props = {
onClick?: (e) => void; onClick?: (e) => void;
}; };
class Block extends Component<Props> { const Block = ({ id, showName, className, onClick }: Props) => {
key: string; const cls = 'block-btn';
cls?: string; const key = 'blocked';
actionDone: string; const activeClass = 'blocked';
action: string; const action = t('block');
activeClass: string; const actionDone = t('blocked');
hoverAction: string; const hoverAction = t('unblock');
constructor() { const [hover, setHover] = useState(false);
super(); const [isBlocked, setIsBlocked] = useState(false);
this.cls = 'block-btn';
this.key = 'blocked';
this.activeClass = 'blocked';
this.action = t('block');
this.actionDone = t('blocked');
this.hoverAction = t('unblock');
this.state = { ...this.state, hover: false };
}
handleMouseEnter = () => { useEffect(() => {
this.setState({ hover: true });
};
handleMouseLeave = () => {
this.setState({ hover: false });
};
onClick(e) {
e.preventDefault();
const newValue = !this.state[this.key];
const hex = Key.toNostrHexAddress(this.props.id);
hex && SocialNetwork.block(hex, newValue);
this.props.onClick?.(e);
}
componentDidMount() {
SocialNetwork.getBlockedUsers((blocks) => { SocialNetwork.getBlockedUsers((blocks) => {
const blocked = blocks?.has(Key.toNostrHexAddress(this.props.id) as string); const blocked = blocks?.has(Key.toNostrHexAddress(id) as string);
this.setState({ blocked }); setIsBlocked(!!blocked);
}); });
}, [id]);
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
const onButtonClick = (e) => {
e.preventDefault();
const newValue = !isBlocked;
const hex = Key.toNostrHexAddress(id);
hex && SocialNetwork.block(hex, newValue);
onClick?.(e);
};
let buttonText;
if (isBlocked && hover) {
buttonText = hoverAction;
} else if (isBlocked && !hover) {
buttonText = actionDone;
} else {
buttonText = action;
} }
render() { return (
const isBlocked = this.state[this.key]; <button
const isHovering = this.state.hover; className={`${cls || key} ${isBlocked ? activeClass : ''} ${className || ''}`}
onClick={onButtonClick}
let buttonText; onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
if (isBlocked && isHovering) { >
buttonText = this.hoverAction; <span>
} else if (isBlocked && !isHovering) { {t(buttonText)} {showName ? <Name pub={id} hideBadge={true} /> : ''}
buttonText = this.actionDone; </span>
} else { </button>
buttonText = this.action; );
} };
return (
<button
className={`${this.cls || this.key} ${isBlocked ? this.activeClass : ''} ${
this.props.className || ''
}`}
onClick={(e) => this.onClick(e)}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<span>
{t(buttonText)} {this.props.showName ? <Name pub={this.props.id} hideBadge={true} /> : ''}
</span>
</button>
);
}
}
export default Block; export default Block;

View File

@ -1,4 +1,5 @@
import Component from '../../BaseComponent'; import { useEffect, useState } from 'react';
import Key from '../../nostr/Key'; import Key from '../../nostr/Key';
import SocialNetwork from '../../nostr/SocialNetwork'; import SocialNetwork from '../../nostr/SocialNetwork';
import { translate as t } from '../../translations/Translation.mjs'; import { translate as t } from '../../translations/Translation.mjs';
@ -8,82 +9,69 @@ type Props = {
className?: string; className?: string;
}; };
class Follow extends Component<Props> { const Follow = ({ id, className }: Props) => {
key: string; const key = 'follow';
cls?: string; const activeClass = 'following';
actionDone: string; const action = t('follow_btn');
action: string; const actionDone = t('following_btn');
activeClass: string; const hoverAction = t('unfollow_btn');
hoverAction: string;
constructor() { const [hover, setHover] = useState(false);
super(); const [isFollowed, setIsFollowed] = useState(false);
this.key = 'follow';
this.activeClass = 'following';
this.action = t('follow_btn');
this.actionDone = t('following_btn');
this.hoverAction = t('unfollow_btn');
this.state = { ...this.state, hover: false };
}
handleMouseEnter = () => { useEffect(() => {
this.setState({ hover: true }); if (key === 'follow') {
};
handleMouseLeave = () => {
this.setState({ hover: false });
};
onClick(e) {
e.preventDefault();
const newValue = !this.state[this.key];
const hex = Key.toNostrHexAddress(this.props.id);
if (!hex) return;
if (this.key === 'follow') {
SocialNetwork.setFollowed(hex, newValue);
return;
}
if (this.key === 'block') {
SocialNetwork.setBlocked(hex, newValue);
}
}
componentDidMount() {
if (this.key === 'follow') {
SocialNetwork.getFollowedByUser(Key.getPubKey(), (follows) => { SocialNetwork.getFollowedByUser(Key.getPubKey(), (follows) => {
const hex = Key.toNostrHexAddress(this.props.id); const hex = Key.toNostrHexAddress(id);
const follow = hex && follows?.has(hex); const follow = hex && follows?.has(hex);
this.setState({ follow }); setIsFollowed(!!follow);
}); });
}
}, [id]);
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
const onClick = (e) => {
e.preventDefault();
const newValue = !isFollowed;
const hex = Key.toNostrHexAddress(id);
if (!hex) return;
if (key === 'follow') {
SocialNetwork.setFollowed(hex, newValue);
setIsFollowed(newValue);
return; return;
} }
} if (key === 'block') {
SocialNetwork.setBlocked(hex, newValue);
render() { setIsFollowed(newValue);
const isFollowed = this.state[this.key];
const isHovering = this.state.hover;
let buttonText;
if (isFollowed && isHovering) {
buttonText = this.hoverAction;
} else if (isFollowed && !isHovering) {
buttonText = this.actionDone;
} else {
buttonText = this.action;
} }
};
return ( let buttonText;
<button if (isFollowed && hover) {
className={`btn ${this.props.className || this.key} ${isFollowed ? this.activeClass : ''}`} buttonText = hoverAction;
onClick={(e) => this.onClick(e)} } else if (isFollowed && !hover) {
onMouseEnter={this.handleMouseEnter} // handle hover state buttonText = actionDone;
onMouseLeave={this.handleMouseLeave} // handle hover state } else {
> buttonText = action;
{t(buttonText)}
</button>
);
} }
}
return (
<button
className={`btn ${className || key} ${isFollowed ? activeClass : ''}`}
onClick={onClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{t(buttonText)}
</button>
);
};
export default Follow; export default Follow;

View File

@ -1,36 +1,74 @@
import { useEffect, useState } from 'react';
import Key from '../../nostr/Key'; import Key from '../../nostr/Key';
import SocialNetwork from '../../nostr/SocialNetwork'; import SocialNetwork from '../../nostr/SocialNetwork';
import { translate as t } from '../../translations/Translation.mjs'; import { translate as t } from '../../translations/Translation.mjs';
import Name from '../user/Name';
import Block from './Block'; type Props = {
id: string;
showName?: boolean;
className?: string;
onClick?: (e) => void;
};
class Report extends Block { const Report = ({ id, showName = false, className, onClick }: Props) => {
constructor() { const cls = 'block'; // changed this from 'block-btn' to 'block'
super(); const key = 'reported'; // key updated for reporting
this.cls = 'block'; const activeClass = 'blocked'; // activeClass remains the same
this.key = 'reported'; const action = t('report_public'); // changed to report_public
this.activeClass = 'blocked'; const actionDone = t('reported'); // changed to reported
this.action = t('report_public'); const hoverAction = t('unreport'); // changed to unreport
this.actionDone = t('reported');
this.hoverAction = t('unreport');
}
onClick(e) { const [hover, setHover] = useState(false);
e.preventDefault(); const [isReported, setIsReported] = useState(false);
const newValue = !this.state[this.key];
if (confirm(newValue ? 'Publicly report this user?' : 'Unreport user?')) {
const hex = Key.toNostrHexAddress(this.props.id);
hex && SocialNetwork.flag(hex, newValue);
}
}
componentDidMount() { useEffect(() => {
SocialNetwork.getFlaggedUsers((flags) => { SocialNetwork.getFlaggedUsers((flags) => {
const hex = Key.toNostrHexAddress(this.props.id); const reported = flags?.has(Key.toNostrHexAddress(id) as string);
const reported = hex && flags?.has(hex); setIsReported(!!reported);
this.setState({ reported });
}); });
}, [id]);
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
const onButtonClick = (e) => {
e.preventDefault();
const newValue = !isReported;
if (window.confirm(newValue ? 'Publicly report this user?' : 'Unreport user?')) {
const hex = Key.toNostrHexAddress(id);
hex && SocialNetwork.flag(hex, newValue);
onClick?.(e);
}
};
let buttonText;
if (isReported && hover) {
buttonText = hoverAction;
} else if (isReported && !hover) {
buttonText = actionDone;
} else {
buttonText = action;
} }
}
return (
<button
className={`${cls || key} ${isReported ? activeClass : ''} ${className || ''}`}
onClick={onButtonClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<span>
{t(buttonText)} {showName ? <Name pub={id} hideBadge={true} /> : ''}
</span>
</button>
);
};
export default Report; export default Report;

View File

@ -1,6 +1,8 @@
import { Link } from 'preact-router'; import { Link } from 'preact-router';
import Component from '../BaseComponent'; import Show from '@/components/helpers/Show.tsx';
import { RouteProps } from '@/views/types.ts';
import Follow from '../components/buttons/Follow'; import Follow from '../components/buttons/Follow';
import Header from '../components/Header'; import Header from '../components/Header';
import Avatar from '../components/user/Avatar'; import Avatar from '../components/user/Avatar';
@ -10,110 +12,98 @@ import Helpers from '../utils/Helpers.tsx';
const IRIS_INFO_ACCOUNT = 'npub1wnwwcv0a8wx0m9stck34ajlwhzuua68ts8mw3kjvspn42dcfyjxs4n95l8'; const IRIS_INFO_ACCOUNT = 'npub1wnwwcv0a8wx0m9stck34ajlwhzuua68ts8mw3kjvspn42dcfyjxs4n95l8';
class About extends Component { const About: React.FC<RouteProps> = () => (
render() { <>
return ( <Header />
<> <div className="main-view prose">
<Header /> <div className="px-2 md:px-4 py-2">
<div className="main-view prose" id="settings"> <h2 className="mt-0">{t('about')}</h2>
<div className="px-2 md:px-4 py-2"> <p>Iris is like the social networking apps we're used to, but better:</p>
<h2 className="mt-0">{t('about')}</h2> <ul>
<p>Iris is like the social networking apps we're used to, but better:</p> <li>
<b>Accessible.</b> No phone number or signup is required. Just type in your name or
alias and go!
</li>
<li>
<b>Secure.</b> It's open source. You can verify that your data stays safe.
</li>
<li>
<b>Always available.</b> It works offline-first and is not dependent on any single
centrally managed server. Users can even connect directly to each other.
</li>
</ul>
<Show when={!Helpers.isStandalone()}>
<h3>Versions</h3>
<p>
<ul> <ul>
<li> <li>
<b>Accessible.</b> No phone number or signup is required. Just type in your name or <a target="_blank" href="https://iris.to">
alias and go! iris.to
</a>{' '}
(web)
</li> </li>
<li> <li>
<b>Secure.</b> It's open source. You can verify that your data stays safe. <a target="_blank" href="https://github.com/irislib/iris-messenger/releases/latest">
Desktop
</a>{' '}
(macOS, Windows, Linux)
</li> </li>
<li> <li>
<b>Always available.</b> It works offline-first and is not dependent on any single <a
centrally managed server. Users can even connect directly to each other. target="_blank"
href="https://apps.apple.com/app/iris-the-nostr-client/id1665849007"
>
iOS
</a>
</li>
<li>
<a target="_blank" href="https://play.google.com/store/apps/details?id=to.iris.twa">
Android
</a>{' '}
(
<a
target="_blank"
href="https://github.com/irislib/iris-messenger/releases/tag/jan2023"
>
apk
</a>
)
</li> </li>
</ul> </ul>
</p>
</Show>
{!Helpers.isStandalone() && ( <h3>Iris docs</h3>
<> <p>
<h3>Versions</h3> Visit Iris <a href="https://docs.iris.to">docs</a> for features, explanations and
<p> troubleshooting.
<ul> </p>
<li>
<a target="_blank" href="https://iris.to">
iris.to
</a>{' '}
(web)
</li>
<li>
<a
target="_blank"
href="https://github.com/irislib/iris-messenger/releases/latest"
>
Desktop
</a>{' '}
(macOS, Windows, Linux)
</li>
<li>
<a
target="_blank"
href="https://apps.apple.com/app/iris-the-nostr-client/id1665849007"
>
iOS
</a>
</li>
<li>
<a
target="_blank"
href="https://play.google.com/store/apps/details?id=to.iris.twa"
>
Android
</a>{' '}
(
<a
target="_blank"
href="https://github.com/irislib/iris-messenger/releases/tag/jan2023"
>
apk
</a>
)
</li>
</ul>
</p>
</>
)}
<h3>Iris docs</h3> <h3>Privacy</h3>
<p> <p>{t('application_security_warning')}</p>
Visit Iris <a href="https://docs.iris.to">docs</a> for features, explanations and
troubleshooting.
</p>
<h3>Privacy</h3> <h3>Follow</h3>
<p>{t('application_security_warning')}</p> <div className="flex flex-row items-center w-full justify-between">
<Link href={`/${IRIS_INFO_ACCOUNT}`} className="flex flex-row items-center gap-2">
<h3>Follow</h3> <Avatar str={IRIS_INFO_ACCOUNT} width={40} />
<div className="flex flex-row items-center w-full justify-between"> <Name pub={IRIS_INFO_ACCOUNT} placeholder="Iris" />
<Link href={`/${IRIS_INFO_ACCOUNT}`} className="flex flex-row items-center gap-2"> </Link>
<Avatar str={IRIS_INFO_ACCOUNT} width={40} /> <Follow className="btn btn-neutral btn-sm" id={IRIS_INFO_ACCOUNT} />
<Name pub={IRIS_INFO_ACCOUNT} placeholder="Iris" />
</Link>
<Follow className="btn btn-neutral btn-sm" id={IRIS_INFO_ACCOUNT} />
</div>
<p>
<a href="https://t.me/irismessenger">Telegram</a> channel.
</p>
<p>
Released under MIT license. Code:{' '}
<a href="https://github.com/irislib/iris-messenger">Github</a>.
</p>
<br />
</div>
</div> </div>
</>
); <p>
} <a href="https://t.me/irismessenger">Telegram</a> channel.
} </p>
<p>
Released under MIT license. Code:{' '}
<a href="https://github.com/irislib/iris-messenger">Github</a>.
</p>
<br />
</div>
</div>
</>
);
export default About; export default About;

View File

@ -1,7 +1,9 @@
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useEffect, useState } from 'preact/hooks';
import { route } from 'preact-router'; import { route } from 'preact-router';
import Component from '../BaseComponent'; import { RouteProps } from '@/views/types.ts';
import Upload from '../components/buttons/Upload'; import Upload from '../components/buttons/Upload';
import Header from '../components/Header'; import Header from '../components/Header';
import SafeImg from '../components/SafeImg'; import SafeImg from '../components/SafeImg';
@ -16,172 +18,153 @@ const explainers = {
nip05: 'Nostr address (nip05)', nip05: 'Nostr address (nip05)',
}; };
export default class EditProfile extends Component { const EditProfile: React.FC<RouteProps> = () => {
constructor(props) { const [profile, setProfile] = useState({});
super(props); const [newFieldName, setNewFieldName] = useState('');
this.state = { const [newFieldValue, setNewFieldValue] = useState('');
profile: {}, const [edited, setEdited] = useState(false);
newFieldName: '',
newFieldValue: '',
edited: false,
};
}
componentDidMount() { useEffect(() => {
SocialNetwork.getProfile(Key.getPubKey(), (p) => { return SocialNetwork.getProfile(Key.getPubKey(), (p) => {
if (!this.state.edited && Object.keys(this.state.profile).length === 0) { if (!edited && Object.keys(profile).length === 0) {
delete p['created_at']; delete p['created_at'];
this.setState({ setProfile(p);
profile: p,
});
} }
}); });
} }, [profile, edited]);
saveOnChange = debounce(() => { const saveOnChange = debounce(() => {
const profile = this.state.profile; const trimmedProfile = { ...profile };
Object.keys(profile).forEach((key) => { Object.keys(trimmedProfile).forEach((key) => {
if (typeof profile[key] === 'string') { if (typeof trimmedProfile[key] === 'string') {
profile[key] = profile[key].trim(); trimmedProfile[key] = trimmedProfile[key].trim();
} }
}); });
SocialNetwork.setMetadata(profile); SocialNetwork.setMetadata(trimmedProfile);
}, 2000); }, 2000);
setProfileAttribute = (key, value) => { const setProfileAttribute = (key, value) => {
key = key.trim(); key = key.trim();
const profile = Object.assign({}, this.state.profile); const updatedProfile = { ...profile };
if (value) { if (value) {
profile[key] = value; updatedProfile[key] = value;
} else { } else {
delete profile[key]; delete updatedProfile[key];
} }
this.setState({ profile, edited: true }); setProfile(updatedProfile);
this.saveOnChange(); setEdited(true);
saveOnChange();
}; };
handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
SocialNetwork.setMetadata(this.state.profile); SocialNetwork.setMetadata(profile);
const myPub = Key.toNostrBech32Address(Key.getPubKey(), 'npub'); const myPub = Key.toNostrBech32Address(Key.getPubKey(), 'npub');
route('/' + myPub); route('/' + myPub);
}; };
handleAddField = (event) => { const handleAddField = (event) => {
event.preventDefault(); event.preventDefault();
const fieldName = this.state.newFieldName; if (newFieldName && newFieldValue) {
const fieldValue = this.state.newFieldValue; setProfileAttribute(newFieldName, newFieldValue);
if (fieldName && fieldValue) { setNewFieldName('');
this.setProfileAttribute(fieldName, fieldValue); setNewFieldValue('');
this.setState({ newFieldName: '', newFieldValue: '' }); SocialNetwork.setMetadata(profile);
SocialNetwork.setMetadata(this.state.profile);
} }
}; };
render() { const fields = ['name', 'picture', 'about', 'banner', 'website', 'lud16', 'nip05'];
const fields = ['name', 'picture', 'about', 'banner', 'website', 'lud16', 'nip05']; Object.keys(profile).forEach((key) => {
// add other possible fields from profile if (!fields.includes(key)) {
Object.keys(this.state.profile).forEach((key) => { fields.push(key);
if (!fields.includes(key)) { }
fields.push(key); });
}
});
return ( return (
<> <>
<Header /> <Header />
<div class="main-view" id="settings"> <div class="mx-2 md:mx-4">
<div class="centered-container prose"> <div class="centered-container prose">
<h3>{t('edit_profile')}</h3> <h3>{t('edit_profile')}</h3>
<form onSubmit={(e) => this.handleSubmit(e)}> <form onSubmit={handleSubmit}>
{fields.map((field) => { {fields.map((field) => {
const val = this.state.profile[field]; const val = profile[field];
const isString = typeof val === 'string' || typeof val === 'undefined'; const isString = typeof val === 'string' || typeof val === 'undefined';
return ( return (
<p> <p>
<label htmlFor={field}>{explainers[field] || field}:</label> <label htmlFor={field}>{explainers[field] || field}:</label>
<br /> <br />
<input <input
className="input w-full" className="input w-full"
type="text" type="text"
id={field} id={field}
disabled={!isString} disabled={!isString}
value={isString ? val || '' : JSON.stringify(val)} value={isString ? val || '' : JSON.stringify(val)}
onInput={(e) => onInput={(e: any) => isString && setProfileAttribute(field, e.target.value)}
isString && />
this.setProfileAttribute(field, (e.target as HTMLInputElement).value) {field === 'lud16' && !val && (
} <p>
/> <small>{t('install_lightning_wallet_prompt')}</small>
{field === 'lud16' && !val && ( </p>
)}
{(field === 'picture' || field === 'banner') && (
<>
<p> <p>
<small>{t('install_lightning_wallet_prompt')}</small> <Upload onUrl={(url) => setProfileAttribute(field, url)} />
</p> </p>
)} {val && (
{field === 'picture' || field === 'banner' ? (
<>
<p> <p>
<Upload onUrl={(url) => this.setProfileAttribute(field, url)} /> <SafeImg key={val} src={val} />
</p> </p>
{val && ( )}
<p> </>
<SafeImg key={val} src={val} /> )}
</p> </p>
)} );
</> })}
) : null} <p>
</p> <button className="btn btn-primary" type="submit">
); Save
})} </button>
<p> </p>
<button className="btn btn-primary" type="submit"> </form>
Save
</button>
</p>
</form>
<h4>Add new field</h4> <h4>Add new field</h4>
<form onSubmit={(e) => this.handleAddField(e)}> <form onSubmit={handleAddField}>
<p> <p>
<label htmlFor="newFieldName">Field name:</label> <label htmlFor="newFieldName">Field name:</label>
<br /> <br />
<input <input
value={this.state.newFieldName} value={newFieldName}
type="text" type="text"
id="newFieldName" id="newFieldName"
className="input w-full" className="input w-full"
placeholder={t('field_name')} placeholder={t('field_name')}
onInput={(e) => onInput={(e: any) => setNewFieldName(e.target.value)}
this.setState({ />
newFieldName: (e.target as HTMLInputElement).value, </p>
}) <p>
} <label htmlFor="newFieldValue">Field value:</label>
/> <br />
</p> <input
<p> value={newFieldValue}
<label htmlFor="newFieldValue">Field value:</label> type="text"
<br /> id="newFieldValue"
<input className="input w-full"
value={this.state.newFieldValue} placeholder={t('field_value')}
type="text" onInput={(e: any) => setNewFieldValue(e.target.value)}
id="newFieldValue" />
className="input w-full" </p>
placeholder={t('field_value')} <p>
onInput={(e) => <button className="btn btn-primary" type="submit">
this.setState({ Add new attribute
newFieldValue: (e.target as HTMLInputElement).value, </button>
}) </p>
} </form>
/>
</p>
<p>
<button className="btn btn-primary" type="submit">
Add new attribute
</button>
</p>
</form>
</div>
</div> </div>
</> </div>
); </>
} );
} };
export default EditProfile;

View File

@ -1,77 +1,73 @@
import Component from '../BaseComponent'; import { RouteProps } from '@/views/types.ts';
import Header from '../components/Header'; import Header from '../components/Header';
import { translate as t } from '../translations/Translation.mjs'; import { translate as t } from '../translations/Translation.mjs';
class Subscribe extends Component { const Subscribe: React.FC<RouteProps> = () => (
render() { <>
return ( <Header />
<> <div className="main-view" id="settings">
<Header /> <div className="centered-container mobile-padding15">
<div className="main-view" id="settings"> <h2>{t('subscribe')}</h2>
<div className="centered-container mobile-padding15"> <h3>Iris Supporter</h3>
<h2>{t('subscribe')}</h2> <p>Support open source development and get extra features!</p>
<h3>Iris Supporter</h3> <p>
<p>Support open source development and get extra features!</p> <ul>
<p> <li>Iris Supporter Badge</li>
<ul> <li>Purple checkmark on Iris</li>
<li>Iris Supporter Badge</li> <li>Iris Supporters' private group chat</li>
<li>Purple checkmark on Iris</li> {/*
<li>High-quality automatic translations (via deepl.com)</li>
<li>Iris Supporters' private group chat</li>
{/*
:D :D
<li>Email-DM bridge for your Iris address</li> <li>Email-DM bridge for your Iris address</li>
<li>Bitcoin Lightning proxy for your Iris address</li> <li>Bitcoin Lightning proxy for your Iris address</li>
<li>Custom themes for your profile page</li> <li>Custom themes for your profile page</li>
<li>Profile view statistics</li> <li>Profile view statistics</li>
*/} */}
<li>More features to come!</li> <li>More features to come!</li>
</ul> </ul>
</p> </p>
<p> <p>
<input <input
defaultChecked={true} defaultChecked={true}
type="radio" type="radio"
id="subscription_annually" id="subscription_annually"
name="subscription" name="subscription"
value="1" value="1"
/> />
<label htmlFor="subscription_annually"> <label htmlFor="subscription_annually">
<b>8 / month</b> charged annually (96 / year) <b>8 / month</b> charged annually (96 / year)
</label> </label>
</p> </p>
<p> <p>
<input type="radio" id="subscription_monthly" name="subscription" value="2" /> <input type="radio" id="subscription_monthly" name="subscription" value="2" />
<label htmlFor="subscription_monthly"> <label htmlFor="subscription_monthly">
<b>10 / month</b> charged monthly (120 / year) <b>10 / month</b> charged monthly (120 / year)
</label> </label>
</p> </p>
<p> <p>
<button className="btn btn-primary">Subscribe</button> <button className="btn btn-primary">Subscribe</button>
</p> </p>
<h3>Iris Titan</h3> <h3>Iris Titan</h3>
<p> <p>
True Mighty Titan status. Lifetime Iris Purple access, plus: True Mighty Titan status. Lifetime Iris Purple access, plus:
<ul> <ul>
<li>Iris Titan Badge</li> <li>Iris Titan Badge</li>
<li>Iris Titans private group chat</li> <li>Iris Titans private group chat</li>
<li>Priority support</li> <li>Priority support</li>
</ul> </ul>
</p> </p>
<p> <p>
<b>1000 </b> one-time payment. <b>1000 </b> one-time payment.
</p> </p>
<p> <p>
<button className="btn btn-primary">Subscribe</button> <button className="btn btn-primary">Subscribe</button>
</p> </p>
<br /> <br />
<br /> <br />
</div> </div>
</div> </div>
</> </>
); );
}
}
export default Subscribe; export default Subscribe;

3
src/js/views/types.ts Normal file
View File

@ -0,0 +1,3 @@
export type RouteProps = {
path: string;
};