basic subscription buttons
This commit is contained in:
parent
2bef05de72
commit
7cfa748269
|
@ -80,6 +80,10 @@ paymentRouter.post("/create-checkout-session", async (req, res, next) => {
|
||||||
return next("not logged in");
|
return next("not logged in");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const source = req.query.source;
|
||||||
|
const returnUrl = source === "dictionary"
|
||||||
|
? "https://dictionary.lingodcs.com/account"
|
||||||
|
: "https://account.lingdocs.com/user";
|
||||||
const price = req.body.priceId;
|
const price = req.body.priceId;
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await stripe.checkout.sessions.create({
|
||||||
billing_address_collection: 'auto',
|
billing_address_collection: 'auto',
|
||||||
|
@ -93,13 +97,13 @@ paymentRouter.post("/create-checkout-session", async (req, res, next) => {
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: req.user.userId,
|
userId: req.user.userId,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
cycle: price === "price_1Lt0XdJnpCQCjf9pM9qqdyt6"
|
cycle: price === "price_1Lt8NqJnpCQCjf9pN7CQUjjO"
|
||||||
? "monthly" : "yearly",
|
? "monthly" : "yearly",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mode: 'subscription',
|
mode: 'subscription',
|
||||||
success_url: `https://account.lingdocs.com/user`,
|
success_url: returnUrl,
|
||||||
cancel_url: `https://account.lingdocs.com/user`,
|
cancel_url: returnUrl,
|
||||||
});
|
});
|
||||||
if (!session.url) {
|
if (!session.url) {
|
||||||
return next("error creating session url");
|
return next("error creating session url");
|
||||||
|
@ -111,10 +115,6 @@ paymentRouter.post("/create-checkout-session", async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
paymentRouter.get("/store", (req, res) => {
|
|
||||||
res.render("store");
|
|
||||||
});
|
|
||||||
|
|
||||||
paymentRouter.post('/create-portal-session', async (req, res, next) => {
|
paymentRouter.post('/create-portal-session', async (req, res, next) => {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return next("error finding user");
|
return next("error finding user");
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="description" content="LingDocs Signin">
|
|
||||||
<title>Store · LingDocs</title>
|
|
||||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous">
|
|
||||||
<script>
|
|
||||||
function handleCancel() {
|
|
||||||
if (window.opener) {
|
|
||||||
const w = window.opener
|
|
||||||
try {
|
|
||||||
w.postMessage("cancelled", "https://dictionary.lingdocs.com");
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
history.back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container py-3">
|
|
||||||
<header>
|
|
||||||
<div class="d-flex flex-column flex-md-row align-items-center pb-3 mb-4 border-bottom">
|
|
||||||
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
|
|
||||||
<span class="fs-4">LingDocs</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<nav class="d-inline-flex mt-2 mt-md-0 ms-md-auto">
|
|
||||||
<a class="py-2 text-dark text-decoration-none" href="https://account.lingdocs.com/">Account</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pricing-header p-3 pb-md-4 mx-auto text-center">
|
|
||||||
<h1 class="display-4 fw-normal">Student Account Pricing</h1>
|
|
||||||
<p class="fs-5 text-muted">Upgrade to a student account and activate a wordlist you can sync accross devices</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="row justify-content-center row-cols-1 row-cols-md-3 mb-3 text-center">
|
|
||||||
<div class="col">
|
|
||||||
<div class="card mb-4 rounded-3 shadow-sm">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h4 class="my-0 fw-normal">Monthly</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h1 class="card-title pricing-card-title">$1<small class="text-muted fw-light">/month</small></h1>
|
|
||||||
<ul class="list-unstyled mt-3 mb-4">
|
|
||||||
<li>Cancel anytime</li>
|
|
||||||
</ul>
|
|
||||||
<form action="/payment/create-checkout-session" method="POST">
|
|
||||||
<input type="hidden" name="priceId" value="price_1Lt0XdJnpCQCjf9pM9qqdyt6" />
|
|
||||||
<button class="w-100 btn btn-lg btn-primary" type="submit">Subscribe</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="card mb-4 rounded-3 shadow-sm">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h4 class="my-0 fw-normal">Yearly</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h1 class="card-title pricing-card-title">$10<small class="text-muted fw-light">/year</small></h1>
|
|
||||||
<ul class="list-unstyled mt-3 mb-4">
|
|
||||||
<li>Save 20%</li>
|
|
||||||
</ul>
|
|
||||||
<form action="/payment/create-checkout-session" method="POST">
|
|
||||||
<input type="hidden" name="priceId" value="price_1Lt0XdJnpCQCjf9pHk1MQqCC" />
|
|
||||||
<button class="w-100 btn btn-lg btn-primary" type="submit">Subscribe</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-4">
|
|
||||||
<button onclick="handleCancel()" type="button" class="w-100 btn btn-lg btn-outline-secondary">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -53,23 +53,18 @@
|
||||||
</form>
|
</form>
|
||||||
<h5>Account Level: <%= user.level.charAt(0).toUpperCase() + user.level.slice(1) %></h5>
|
<h5>Account Level: <%= user.level.charAt(0).toUpperCase() + user.level.slice(1) %></h5>
|
||||||
<% if (user.level === "basic") { %>
|
<% if (user.level === "basic") { %>
|
||||||
<% if (user.upgradeToStudentRequest === "waiting") { %>
|
|
||||||
<p>Wating for upgrade approval</p>
|
|
||||||
<% } %>
|
|
||||||
<% if (user.tester) { %>
|
|
||||||
<p>Upgrade to Student account for Wordlist features</p>
|
<p>Upgrade to Student account for Wordlist features</p>
|
||||||
<div class="d-flex flex-row" style="gap: 1rem;">
|
<div class="d-flex flex-row" style="gap: 1rem;">
|
||||||
<form action="/payment/create-checkout-session" method="POST">
|
<form action="/payment/create-checkout-session" method="POST">
|
||||||
<input type="hidden" name="priceId" value="price_1Lt0XdJnpCQCjf9pM9qqdyt6" />
|
<input type="hidden" name="priceId" value="price_1Lt8NqJnpCQCjf9pN7CQUjjO" />
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="submit">$1/month</button>
|
<button class="btn btn-sm btn-outline-secondary" type="submit">$1/month</button>
|
||||||
</form>
|
</form>
|
||||||
<form action="/payment/create-checkout-session" method="POST">
|
<form action="/payment/create-checkout-session" method="POST">
|
||||||
<input type="hidden" name="priceId" value="price_1Lt0XdJnpCQCjf9pHk1MQqCC" />
|
<input type="hidden" name="priceId" value="price_1Lt8NqJnpCQCjf9p4FAEIOMw" />
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="submit">$10/year</button>
|
<button class="btn btn-sm btn-outline-secondary" type="submit">$10/year</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% } %>
|
|
||||||
<% if (user.tester && user.level === "student" && user.subscription) { %>
|
<% if (user.tester && user.level === "student" && user.subscription) { %>
|
||||||
<p>Current subscription: <% if (user.subscription.metadata.cycle === "monthly") { %>$1/month<% } else { %>$10/year<% } %></p>
|
<p>Current subscription: <% if (user.subscription.metadata.cycle === "monthly") { %>$1/month<% } else { %>$10/year<% } %></p>
|
||||||
<p><a href="#" onclick="handleDowngrade()">Downgrade</a> to cancel your subscription</p>
|
<p><a href="#" onclick="handleDowngrade()">Downgrade</a> to cancel your subscription</p>
|
||||||
|
|
|
@ -25,7 +25,6 @@ import EntryEditor from "./screens/EntryEditor";
|
||||||
import IsolatedEntry from "./screens/IsolatedEntry";
|
import IsolatedEntry from "./screens/IsolatedEntry";
|
||||||
import PrivacyPolicy from "./screens/PrivacyPolicy";
|
import PrivacyPolicy from "./screens/PrivacyPolicy";
|
||||||
import Wordlist from "./screens/Wordlist";
|
import Wordlist from "./screens/Wordlist";
|
||||||
import { wordlistEnabled } from "./lib/level-management";
|
|
||||||
import {
|
import {
|
||||||
saveOptions,
|
saveOptions,
|
||||||
readOptions,
|
readOptions,
|
||||||
|
@ -585,14 +584,15 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
isolateEntry={this.handleIsolateEntry}
|
isolateEntry={this.handleIsolateEntry}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{wordlistEnabled(this.state.user) && <Route path="/wordlist">
|
<Route path="/wordlist">
|
||||||
<Wordlist
|
<Wordlist
|
||||||
options={this.state.options}
|
options={this.state.options}
|
||||||
wordlist={this.state.wordlist}
|
wordlist={this.state.wordlist}
|
||||||
isolateEntry={this.handleIsolateEntry}
|
isolateEntry={this.handleIsolateEntry}
|
||||||
optionsDispatch={this.handleOptionsUpdate}
|
optionsDispatch={this.handleOptionsUpdate}
|
||||||
|
user={this.state.user}
|
||||||
/>
|
/>
|
||||||
</Route>}
|
</Route>
|
||||||
{this.state.user?.level === "editor" && <Route path="/edit">
|
{this.state.user?.level === "editor" && <Route path="/edit">
|
||||||
<EntryEditor
|
<EntryEditor
|
||||||
isolatedEntry={this.state.isolatedEntry}
|
isolatedEntry={this.state.isolatedEntry}
|
||||||
|
@ -619,13 +619,11 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
<BottomNavItem label="About" icon="info-circle" page="/about" />
|
<BottomNavItem label="About" icon="info-circle" page="/about" />
|
||||||
<BottomNavItem label="Settings" icon="cog" page="/settings" />
|
<BottomNavItem label="Settings" icon="cog" page="/settings" />
|
||||||
<BottomNavItem label={this.state.user ? "Account" : "Sign In"} icon="user" page="/account" />
|
<BottomNavItem label={this.state.user ? "Account" : "Sign In"} icon="user" page="/account" />
|
||||||
{wordlistEnabled(this.state.user) &&
|
|
||||||
<BottomNavItem
|
<BottomNavItem
|
||||||
label={`Wordlist ${this.state.options.wordlistReviewBadge ? textBadge(forReview(this.state.wordlist).length) : ""}`}
|
label={`Wordlist ${this.state.options.wordlistReviewBadge ? textBadge(forReview(this.state.wordlist).length) : ""}`}
|
||||||
icon="list"
|
icon="list"
|
||||||
page="/wordlist"
|
page="/wordlist"
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
{this.state.user?.level === "editor" &&
|
{this.state.user?.level === "editor" &&
|
||||||
<BottomNavItem
|
<BottomNavItem
|
||||||
label={`Tasks ${textBadge(this.state.reviewTasks.length)}`}
|
label={`Tasks ${textBadge(this.state.reviewTasks.length)}`}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
const prices = [
|
||||||
|
{
|
||||||
|
label: "$1/month",
|
||||||
|
priceId: "price_1Lt8NqJnpCQCjf9pN7CQUjjO",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "$10/year",
|
||||||
|
priceId: "price_1Lt8NqJnpCQCjf9p4FAEIOMw",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const checkoutPortalUrl = "https://account.lingdocs.com/payment/create-checkout-session/?source=dictionary";
|
||||||
|
|
||||||
|
function UpgradePrices() {
|
||||||
|
return <div className="my-4">
|
||||||
|
<h5>Subscription options</h5>
|
||||||
|
<div className="d-flex flex-row flex-wrap my-3" style={{ gap: "1.5rem" }}>
|
||||||
|
{prices.map(({ priceId, label }) => <div key={priceId}>
|
||||||
|
<form
|
||||||
|
action={checkoutPortalUrl}
|
||||||
|
method="POST"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="priceId" value={priceId} />
|
||||||
|
<button className="btn btn-primary" type="submit">{label}</button>
|
||||||
|
</form>
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpgradePrices;
|
|
@ -7,11 +7,11 @@ import {
|
||||||
upgradeAccount,
|
upgradeAccount,
|
||||||
signOut,
|
signOut,
|
||||||
publishDictionary,
|
publishDictionary,
|
||||||
upgradeToStudentRequest,
|
|
||||||
} from "../lib/backend-calls";
|
} from "../lib/backend-calls";
|
||||||
import LoadingElipses from "../components/LoadingElipses";
|
import LoadingElipses from "../components/LoadingElipses";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import * as AT from "../types/account-types";
|
import * as AT from "../types/account-types";
|
||||||
|
import UpgradePrices from "../components/UpgradePrices";
|
||||||
|
|
||||||
const providers: ("google" | "twitter" | "github")[] = ["google", "twitter", "github"];
|
const providers: ("google" | "twitter" | "github")[] = ["google", "twitter", "github"];
|
||||||
|
|
||||||
|
@ -58,22 +58,6 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs
|
||||||
setUpgradePassword("");
|
setUpgradePassword("");
|
||||||
setUpgradeError("");
|
setUpgradeError("");
|
||||||
}
|
}
|
||||||
async function handleUpgradeRequest() {
|
|
||||||
setUpgradeError("");
|
|
||||||
setWaiting(true);
|
|
||||||
upgradeToStudentRequest().then((res) => {
|
|
||||||
setWaiting(false);
|
|
||||||
if (res.ok) {
|
|
||||||
loadUser();
|
|
||||||
closeUpgrade();
|
|
||||||
} else {
|
|
||||||
setUpgradeError("Error requesting upgrade");
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
setWaiting(false);
|
|
||||||
setUpgradeError(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async function handleUpgrade() {
|
async function handleUpgrade() {
|
||||||
setUpgradeError("");
|
setUpgradeError("");
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
|
@ -161,9 +145,7 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>}
|
</li>}
|
||||||
<li className="list-group-item">Account Level: {capitalize(user.level)} {user.upgradeToStudentRequest === "waiting"
|
<li className="list-group-item">Account Level: {capitalize(user.level)}</li>
|
||||||
? "(Upgrade Requested)"
|
|
||||||
: ""}</li>
|
|
||||||
<li className="list-group-item">Signs in with:
|
<li className="list-group-item">Signs in with:
|
||||||
{(user.password && user.email) && <span>
|
{(user.password && user.email) && <span>
|
||||||
<i className="fas fa-key ml-2"></i> <span className="small mr-1">Password</span>
|
<i className="fas fa-key ml-2"></i> <span className="small mr-1">Password</span>
|
||||||
|
@ -206,9 +188,19 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>Upgrade to Student Account</Modal.Title>
|
<Modal.Title>Upgrade to Student Account</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>Enter the secret upgrade password to upgrade your account or <button className="btn btn-sm btn-outline-secondary my-2" onClick={handleUpgradeRequest}>request an upgrade</button></Modal.Body>
|
<Modal.Body>
|
||||||
|
<p className="lead">Upgrade to a <strong>student account</strong> to enable the wordlist</p>
|
||||||
|
<p>Features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Save your wordlist and sync across devices</li>
|
||||||
|
<li>Save text, audio, or visual context for words</li>
|
||||||
|
<li>Review words with Anki-style spaced repetition</li>
|
||||||
|
</ul>
|
||||||
|
<UpgradePrices />
|
||||||
|
<p>Or enter upgrade password</p>
|
||||||
|
</Modal.Body>
|
||||||
<div className="form-group px-3">
|
<div className="form-group px-3">
|
||||||
<label htmlFor="upgradePasswordForm">Upgrade password:</label>
|
<label htmlFor="upgradePasswordForm">Upgrade token:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Entry from "../components/Entry";
|
import Entry from "../components/Entry";
|
||||||
|
import * as AT from "../types/account-types";
|
||||||
import WordlistWordEditor from "../components/WordlistWordEditor";
|
import WordlistWordEditor from "../components/WordlistWordEditor";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Helmet from "react-helmet";
|
import Helmet from "react-helmet";
|
||||||
|
@ -44,6 +45,7 @@ import AudioPlayButton from "../components/AudioPlayButton";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime.js";
|
import relativeTime from "dayjs/plugin/relativeTime.js";
|
||||||
import hitBottom from "../lib/hitBottom";
|
import hitBottom from "../lib/hitBottom";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Options,
|
Options,
|
||||||
WordlistWord,
|
WordlistWord,
|
||||||
|
@ -51,6 +53,7 @@ import {
|
||||||
OptionsAction,
|
OptionsAction,
|
||||||
WordlistMode,
|
WordlistMode,
|
||||||
} from "../types/dictionary-types";
|
} from "../types/dictionary-types";
|
||||||
|
import UpgradePrices from "../components/UpgradePrices";
|
||||||
|
|
||||||
const cleanupIcon = "broom";
|
const cleanupIcon = "broom";
|
||||||
|
|
||||||
|
@ -81,11 +84,12 @@ function amountOfWords(number: number): string {
|
||||||
return `${number} word${number !== 1 ? "s" : ""}`;
|
return `${number} word${number !== 1 ? "s" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Wordlist({ options, wordlist, isolateEntry, optionsDispatch }: {
|
function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user }: {
|
||||||
options: Options,
|
options: Options,
|
||||||
wordlist: WordlistWord[],
|
wordlist: WordlistWord[],
|
||||||
isolateEntry: (ts: number) => void,
|
isolateEntry: (ts: number) => void,
|
||||||
optionsDispatch: (action: OptionsAction) => void,
|
optionsDispatch: (action: OptionsAction) => void,
|
||||||
|
user: AT.LingdocsUser | undefined,
|
||||||
}) {
|
}) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const [wordOpen, setWordOpen] = useState<string | undefined>(undefined);
|
const [wordOpen, setWordOpen] = useState<string | undefined>(undefined);
|
||||||
|
@ -253,6 +257,31 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch }: {
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
if (!(user?.level === "student")) {
|
||||||
|
return <div className="width-limiter" style={{ marginBottom: "120px" }}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Wordlist - LingDocs Pashto Dictionary</title>
|
||||||
|
</Helmet>
|
||||||
|
<div className="d-flex flex-row justify-content-between mb-2">
|
||||||
|
<h4 className="mb-3">Wordlist</h4>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: "2rem" }}>
|
||||||
|
{!user
|
||||||
|
? <p className="lead"><Link to="/account">Sign in</Link> to upgrade and enable wordlist</p>
|
||||||
|
: <div>
|
||||||
|
<p className="lead">Upgrade to a <strong>student account</strong> to enable the wordlist</p>
|
||||||
|
<p>Features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Save your wordlist and sync across devices</li>
|
||||||
|
<li>Save text, audio, or visual context for words</li>
|
||||||
|
<li>Review words with Anki-style spaced repetition</li>
|
||||||
|
</ul>
|
||||||
|
<UpgradePrices />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
return <div className="width-limiter" style={{ marginBottom: "120px" }}>
|
return <div className="width-limiter" style={{ marginBottom: "120px" }}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Wordlist - LingDocs Pashto Dictionary</title>
|
<title>Wordlist - LingDocs Pashto Dictionary</title>
|
||||||
|
|
Loading…
Reference in New Issue