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'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'simple-import-sort/imports': [
'error',
{

View File

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

View File

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

View File

@ -1,36 +1,74 @@
import { useEffect, useState } from 'react';
import Key from '../../nostr/Key';
import SocialNetwork from '../../nostr/SocialNetwork';
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 {
constructor() {
super();
this.cls = 'block';
this.key = 'reported';
this.activeClass = 'blocked';
this.action = t('report_public');
this.actionDone = t('reported');
this.hoverAction = t('unreport');
}
const Report = ({ id, showName = false, className, onClick }: Props) => {
const cls = 'block'; // changed this from 'block-btn' to 'block'
const key = 'reported'; // key updated for reporting
const activeClass = 'blocked'; // activeClass remains the same
const action = t('report_public'); // changed to report_public
const actionDone = t('reported'); // changed to reported
const hoverAction = t('unreport'); // changed to unreport
onClick(e) {
e.preventDefault();
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);
}
}
const [hover, setHover] = useState(false);
const [isReported, setIsReported] = useState(false);
componentDidMount() {
useEffect(() => {
SocialNetwork.getFlaggedUsers((flags) => {
const hex = Key.toNostrHexAddress(this.props.id);
const reported = hex && flags?.has(hex);
this.setState({ reported });
const reported = flags?.has(Key.toNostrHexAddress(id) as string);
setIsReported(!!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;

View File

@ -1,6 +1,8 @@
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 Header from '../components/Header';
import Avatar from '../components/user/Avatar';
@ -10,110 +12,98 @@ import Helpers from '../utils/Helpers.tsx';
const IRIS_INFO_ACCOUNT = 'npub1wnwwcv0a8wx0m9stck34ajlwhzuua68ts8mw3kjvspn42dcfyjxs4n95l8';
class About extends Component {
render() {
return (
<>
<Header />
<div className="main-view prose" id="settings">
<div className="px-2 md:px-4 py-2">
<h2 className="mt-0">{t('about')}</h2>
<p>Iris is like the social networking apps we're used to, but better:</p>
const About: React.FC<RouteProps> = () => (
<>
<Header />
<div className="main-view prose">
<div className="px-2 md:px-4 py-2">
<h2 className="mt-0">{t('about')}</h2>
<p>Iris is like the social networking apps we're used to, but better:</p>
<ul>
<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>
<li>
<b>Accessible.</b> No phone number or signup is required. Just type in your name or
alias and go!
<a target="_blank" href="https://iris.to">
iris.to
</a>{' '}
(web)
</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>
<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.
<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>
</Show>
{!Helpers.isStandalone() && (
<>
<h3>Versions</h3>
<p>
<ul>
<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>
<p>
Visit Iris <a href="https://docs.iris.to">docs</a> for features, explanations and
troubleshooting.
</p>
<h3>Iris docs</h3>
<p>
Visit Iris <a href="https://docs.iris.to">docs</a> for features, explanations and
troubleshooting.
</p>
<h3>Privacy</h3>
<p>{t('application_security_warning')}</p>
<h3>Privacy</h3>
<p>{t('application_security_warning')}</p>
<h3>Follow</h3>
<div className="flex flex-row items-center w-full justify-between">
<Link href={`/${IRIS_INFO_ACCOUNT}`} className="flex flex-row items-center gap-2">
<Avatar str={IRIS_INFO_ACCOUNT} width={40} />
<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>
<h3>Follow</h3>
<div className="flex flex-row items-center w-full justify-between">
<Link href={`/${IRIS_INFO_ACCOUNT}`} className="flex flex-row items-center gap-2">
<Avatar str={IRIS_INFO_ACCOUNT} width={40} />
<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>
</>
);
export default About;

View File

@ -1,7 +1,9 @@
import debounce from 'lodash/debounce';
import { useEffect, useState } from 'preact/hooks';
import { route } from 'preact-router';
import Component from '../BaseComponent';
import { RouteProps } from '@/views/types.ts';
import Upload from '../components/buttons/Upload';
import Header from '../components/Header';
import SafeImg from '../components/SafeImg';
@ -16,172 +18,153 @@ const explainers = {
nip05: 'Nostr address (nip05)',
};
export default class EditProfile extends Component {
constructor(props) {
super(props);
this.state = {
profile: {},
newFieldName: '',
newFieldValue: '',
edited: false,
};
}
const EditProfile: React.FC<RouteProps> = () => {
const [profile, setProfile] = useState({});
const [newFieldName, setNewFieldName] = useState('');
const [newFieldValue, setNewFieldValue] = useState('');
const [edited, setEdited] = useState(false);
componentDidMount() {
SocialNetwork.getProfile(Key.getPubKey(), (p) => {
if (!this.state.edited && Object.keys(this.state.profile).length === 0) {
useEffect(() => {
return SocialNetwork.getProfile(Key.getPubKey(), (p) => {
if (!edited && Object.keys(profile).length === 0) {
delete p['created_at'];
this.setState({
profile: p,
});
setProfile(p);
}
});
}
}, [profile, edited]);
saveOnChange = debounce(() => {
const profile = this.state.profile;
Object.keys(profile).forEach((key) => {
if (typeof profile[key] === 'string') {
profile[key] = profile[key].trim();
const saveOnChange = debounce(() => {
const trimmedProfile = { ...profile };
Object.keys(trimmedProfile).forEach((key) => {
if (typeof trimmedProfile[key] === 'string') {
trimmedProfile[key] = trimmedProfile[key].trim();
}
});
SocialNetwork.setMetadata(profile);
SocialNetwork.setMetadata(trimmedProfile);
}, 2000);
setProfileAttribute = (key, value) => {
const setProfileAttribute = (key, value) => {
key = key.trim();
const profile = Object.assign({}, this.state.profile);
const updatedProfile = { ...profile };
if (value) {
profile[key] = value;
updatedProfile[key] = value;
} else {
delete profile[key];
delete updatedProfile[key];
}
this.setState({ profile, edited: true });
this.saveOnChange();
setProfile(updatedProfile);
setEdited(true);
saveOnChange();
};
handleSubmit = (event) => {
const handleSubmit = (event) => {
event.preventDefault();
SocialNetwork.setMetadata(this.state.profile);
SocialNetwork.setMetadata(profile);
const myPub = Key.toNostrBech32Address(Key.getPubKey(), 'npub');
route('/' + myPub);
};
handleAddField = (event) => {
const handleAddField = (event) => {
event.preventDefault();
const fieldName = this.state.newFieldName;
const fieldValue = this.state.newFieldValue;
if (fieldName && fieldValue) {
this.setProfileAttribute(fieldName, fieldValue);
this.setState({ newFieldName: '', newFieldValue: '' });
SocialNetwork.setMetadata(this.state.profile);
if (newFieldName && newFieldValue) {
setProfileAttribute(newFieldName, newFieldValue);
setNewFieldName('');
setNewFieldValue('');
SocialNetwork.setMetadata(profile);
}
};
render() {
const fields = ['name', 'picture', 'about', 'banner', 'website', 'lud16', 'nip05'];
// add other possible fields from profile
Object.keys(this.state.profile).forEach((key) => {
if (!fields.includes(key)) {
fields.push(key);
}
});
const fields = ['name', 'picture', 'about', 'banner', 'website', 'lud16', 'nip05'];
Object.keys(profile).forEach((key) => {
if (!fields.includes(key)) {
fields.push(key);
}
});
return (
<>
<Header />
<div class="main-view" id="settings">
<div class="centered-container prose">
<h3>{t('edit_profile')}</h3>
<form onSubmit={(e) => this.handleSubmit(e)}>
{fields.map((field) => {
const val = this.state.profile[field];
const isString = typeof val === 'string' || typeof val === 'undefined';
return (
<p>
<label htmlFor={field}>{explainers[field] || field}:</label>
<br />
<input
className="input w-full"
type="text"
id={field}
disabled={!isString}
value={isString ? val || '' : JSON.stringify(val)}
onInput={(e) =>
isString &&
this.setProfileAttribute(field, (e.target as HTMLInputElement).value)
}
/>
{field === 'lud16' && !val && (
return (
<>
<Header />
<div class="mx-2 md:mx-4">
<div class="centered-container prose">
<h3>{t('edit_profile')}</h3>
<form onSubmit={handleSubmit}>
{fields.map((field) => {
const val = profile[field];
const isString = typeof val === 'string' || typeof val === 'undefined';
return (
<p>
<label htmlFor={field}>{explainers[field] || field}:</label>
<br />
<input
className="input w-full"
type="text"
id={field}
disabled={!isString}
value={isString ? val || '' : JSON.stringify(val)}
onInput={(e: any) => isString && setProfileAttribute(field, e.target.value)}
/>
{field === 'lud16' && !val && (
<p>
<small>{t('install_lightning_wallet_prompt')}</small>
</p>
)}
{(field === 'picture' || field === 'banner') && (
<>
<p>
<small>{t('install_lightning_wallet_prompt')}</small>
<Upload onUrl={(url) => setProfileAttribute(field, url)} />
</p>
)}
{field === 'picture' || field === 'banner' ? (
<>
{val && (
<p>
<Upload onUrl={(url) => this.setProfileAttribute(field, url)} />
<SafeImg key={val} src={val} />
</p>
{val && (
<p>
<SafeImg key={val} src={val} />
</p>
)}
</>
) : null}
</p>
);
})}
<p>
<button className="btn btn-primary" type="submit">
Save
</button>
</p>
</form>
)}
</>
)}
</p>
);
})}
<p>
<button className="btn btn-primary" type="submit">
Save
</button>
</p>
</form>
<h4>Add new field</h4>
<form onSubmit={(e) => this.handleAddField(e)}>
<p>
<label htmlFor="newFieldName">Field name:</label>
<br />
<input
value={this.state.newFieldName}
type="text"
id="newFieldName"
className="input w-full"
placeholder={t('field_name')}
onInput={(e) =>
this.setState({
newFieldName: (e.target as HTMLInputElement).value,
})
}
/>
</p>
<p>
<label htmlFor="newFieldValue">Field value:</label>
<br />
<input
value={this.state.newFieldValue}
type="text"
id="newFieldValue"
className="input w-full"
placeholder={t('field_value')}
onInput={(e) =>
this.setState({
newFieldValue: (e.target as HTMLInputElement).value,
})
}
/>
</p>
<p>
<button className="btn btn-primary" type="submit">
Add new attribute
</button>
</p>
</form>
</div>
<h4>Add new field</h4>
<form onSubmit={handleAddField}>
<p>
<label htmlFor="newFieldName">Field name:</label>
<br />
<input
value={newFieldName}
type="text"
id="newFieldName"
className="input w-full"
placeholder={t('field_name')}
onInput={(e: any) => setNewFieldName(e.target.value)}
/>
</p>
<p>
<label htmlFor="newFieldValue">Field value:</label>
<br />
<input
value={newFieldValue}
type="text"
id="newFieldValue"
className="input w-full"
placeholder={t('field_value')}
onInput={(e: any) => setNewFieldValue(e.target.value)}
/>
</p>
<p>
<button className="btn btn-primary" type="submit">
Add new attribute
</button>
</p>
</form>
</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 { translate as t } from '../translations/Translation.mjs';
class Subscribe extends Component {
render() {
return (
<>
<Header />
<div className="main-view" id="settings">
<div className="centered-container mobile-padding15">
<h2>{t('subscribe')}</h2>
<h3>Iris Supporter</h3>
<p>Support open source development and get extra features!</p>
<p>
<ul>
<li>Iris Supporter Badge</li>
<li>Purple checkmark on Iris</li>
<li>High-quality automatic translations (via deepl.com)</li>
<li>Iris Supporters' private group chat</li>
{/*
const Subscribe: React.FC<RouteProps> = () => (
<>
<Header />
<div className="main-view" id="settings">
<div className="centered-container mobile-padding15">
<h2>{t('subscribe')}</h2>
<h3>Iris Supporter</h3>
<p>Support open source development and get extra features!</p>
<p>
<ul>
<li>Iris Supporter Badge</li>
<li>Purple checkmark on Iris</li>
<li>Iris Supporters' private group chat</li>
{/*
:D
<li>Email-DM bridge for your Iris address</li>
<li>Bitcoin Lightning proxy for your Iris address</li>
<li>Custom themes for your profile page</li>
<li>Profile view statistics</li>
*/}
<li>More features to come!</li>
</ul>
</p>
<p>
<input
defaultChecked={true}
type="radio"
id="subscription_annually"
name="subscription"
value="1"
/>
<label htmlFor="subscription_annually">
<b>8 / month</b> charged annually (96 / year)
</label>
</p>
<p>
<input type="radio" id="subscription_monthly" name="subscription" value="2" />
<label htmlFor="subscription_monthly">
<b>10 / month</b> charged monthly (120 / year)
</label>
</p>
<p>
<button className="btn btn-primary">Subscribe</button>
</p>
<li>More features to come!</li>
</ul>
</p>
<p>
<input
defaultChecked={true}
type="radio"
id="subscription_annually"
name="subscription"
value="1"
/>
<label htmlFor="subscription_annually">
<b>8 / month</b> charged annually (96 / year)
</label>
</p>
<p>
<input type="radio" id="subscription_monthly" name="subscription" value="2" />
<label htmlFor="subscription_monthly">
<b>10 / month</b> charged monthly (120 / year)
</label>
</p>
<p>
<button className="btn btn-primary">Subscribe</button>
</p>
<h3>Iris Titan</h3>
<p>
True Mighty Titan status. Lifetime Iris Purple access, plus:
<ul>
<li>Iris Titan Badge</li>
<li>Iris Titans private group chat</li>
<li>Priority support</li>
</ul>
</p>
<p>
<b>1000 </b> one-time payment.
</p>
<p>
<button className="btn btn-primary">Subscribe</button>
</p>
<br />
<br />
</div>
</div>
</>
);
}
}
<h3>Iris Titan</h3>
<p>
True Mighty Titan status. Lifetime Iris Purple access, plus:
<ul>
<li>Iris Titan Badge</li>
<li>Iris Titans private group chat</li>
<li>Priority support</li>
</ul>
</p>
<p>
<b>1000 </b> one-time payment.
</p>
<p>
<button className="btn btn-primary">Subscribe</button>
</p>
<br />
<br />
</div>
</div>
</>
);
export default Subscribe;

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

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