mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-09 20:57:43 +00:00
add optimistic updates to example
This commit is contained in:
@@ -5,14 +5,25 @@ import { AlertCircle } from "lucide-react";
|
|||||||
import { useMutation, useQuery, useAction } from "convex/react";
|
import { useMutation, useQuery, useAction } from "convex/react";
|
||||||
import { api } from "../convex/_generated/api";
|
import { api } from "../convex/_generated/api";
|
||||||
import { CheckoutLink, CustomerPortalLink } from "../../src/react";
|
import { CheckoutLink, CustomerPortalLink } from "../../src/react";
|
||||||
|
import {
|
||||||
|
insertTodoOptimistic,
|
||||||
|
completeTodoOptimistic,
|
||||||
|
deleteTodoOptimistic,
|
||||||
|
} from "@/optimistic";
|
||||||
|
|
||||||
export default function TodoList() {
|
export default function TodoList() {
|
||||||
const user = useQuery(api.example.getCurrentUser);
|
const user = useQuery(api.example.getCurrentUser);
|
||||||
const todos = useQuery(api.example.listTodos);
|
const todos = useQuery(api.example.listTodos);
|
||||||
const products = useQuery(api.example.getConfiguredProducts);
|
const products = useQuery(api.example.getConfiguredProducts);
|
||||||
const insertTodo = useMutation(api.example.insertTodo);
|
const insertTodo = useMutation(api.example.insertTodo).withOptimisticUpdate(
|
||||||
const completeTodo = useMutation(api.example.completeTodo);
|
insertTodoOptimistic
|
||||||
const deleteTodo = useMutation(api.example.deleteTodo);
|
);
|
||||||
|
const completeTodo = useMutation(
|
||||||
|
api.example.completeTodo
|
||||||
|
).withOptimisticUpdate(completeTodoOptimistic);
|
||||||
|
const deleteTodo = useMutation(api.example.deleteTodo).withOptimisticUpdate(
|
||||||
|
deleteTodoOptimistic
|
||||||
|
);
|
||||||
const cancelSubscription = useAction(api.example.cancelCurrentSubscription);
|
const cancelSubscription = useAction(api.example.cancelCurrentSubscription);
|
||||||
const changeSubscription = useAction(api.example.changeCurrentSubscription);
|
const changeSubscription = useAction(api.example.changeCurrentSubscription);
|
||||||
const [newTodo, setNewTodo] = useState("");
|
const [newTodo, setNewTodo] = useState("");
|
||||||
@@ -84,6 +95,14 @@ export default function TodoList() {
|
|||||||
<h1 className="text-3xl font-light mb-6 text-gray-800 dark:text-gray-100">
|
<h1 className="text-3xl font-light mb-6 text-gray-800 dark:text-gray-100">
|
||||||
Todo List
|
Todo List
|
||||||
</h1>
|
</h1>
|
||||||
|
<div className="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<p>Plan limits:</p>
|
||||||
|
<ul className="list-disc list-inside mt-1">
|
||||||
|
<li>Free: Up to 3 todos</li>
|
||||||
|
<li>Premium: Up to 6 todos</li>
|
||||||
|
<li>Premium Plus: Unlimited todos</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<form onSubmit={addTodo} className="mb-6">
|
<form onSubmit={addTodo} className="mb-6">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -169,6 +188,14 @@ export default function TodoList() {
|
|||||||
{user.subscription.recurringInterval}
|
{user.subscription.recurringInterval}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
•{" "}
|
||||||
|
{user?.isPremiumPlus
|
||||||
|
? "Unlimited todos"
|
||||||
|
: user?.isPremium
|
||||||
|
? "Up to 6 todos"
|
||||||
|
: "Up to 3 todos"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,17 +212,25 @@ export default function TodoList() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium">Premium</h4>
|
<h4 className="font-medium">Premium</h4>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="flex items-center gap-2">
|
||||||
${(premiumMonthly.prices[0].priceAmount ?? 0) / 100}
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
/month
|
|
||||||
</p>
|
|
||||||
{premiumYearly && (
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
$
|
$
|
||||||
{(premiumYearly.prices[0].priceAmount ?? 0) / 100}
|
{(premiumMonthly.prices[0].priceAmount ?? 0) /
|
||||||
/year
|
100}
|
||||||
</p>
|
/month
|
||||||
)}
|
</span>
|
||||||
|
{premiumYearly && (
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
or $
|
||||||
|
{(premiumYearly.prices[0].priceAmount ?? 0) /
|
||||||
|
100}
|
||||||
|
/year
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
• Up to 6 todos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.subscription?.productId !== premiumMonthly.id &&
|
{user?.subscription?.productId !== premiumMonthly.id &&
|
||||||
@@ -212,7 +247,7 @@ export default function TodoList() {
|
|||||||
className="text-sm text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
|
className="text-sm text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
embed={false}
|
embed={false}
|
||||||
>
|
>
|
||||||
Upgrade to Premium
|
Upgrade to Premium (redirect)
|
||||||
</CheckoutLink>
|
</CheckoutLink>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -232,20 +267,25 @@ export default function TodoList() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium">Premium Plus</h4>
|
<h4 className="font-medium">Premium Plus</h4>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="flex items-center gap-2">
|
||||||
$
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{(premiumPlusMonthly.prices[0].priceAmount ?? 0) /
|
|
||||||
100}
|
|
||||||
/month
|
|
||||||
</p>
|
|
||||||
{premiumPlusYearly && (
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
$
|
$
|
||||||
{(premiumPlusYearly.prices[0].priceAmount ?? 0) /
|
{(premiumPlusMonthly.prices[0].priceAmount ?? 0) /
|
||||||
100}
|
100}
|
||||||
/year
|
/month
|
||||||
</p>
|
</span>
|
||||||
)}
|
{premiumPlusYearly && (
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
or $
|
||||||
|
{(premiumPlusYearly.prices[0].priceAmount ??
|
||||||
|
0) / 100}
|
||||||
|
/year
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
• Unlimited todos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.subscription?.productId !==
|
{user?.subscription?.productId !==
|
||||||
@@ -262,7 +302,7 @@ export default function TodoList() {
|
|||||||
].filter((id): id is string => id !== undefined)}
|
].filter((id): id is string => id !== undefined)}
|
||||||
className="text-sm text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
|
className="text-sm text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
>
|
>
|
||||||
Upgrade to Premium Plus
|
Upgrade to Premium Plus (modal)
|
||||||
</CheckoutLink>
|
</CheckoutLink>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
57
example/src/optimistic.ts
Normal file
57
example/src/optimistic.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { api } from "../convex/_generated/api";
|
||||||
|
import { Id } from "../convex/_generated/dataModel";
|
||||||
|
import { OptimisticUpdate } from "convex/browser";
|
||||||
|
|
||||||
|
export const insertTodoOptimistic: OptimisticUpdate<
|
||||||
|
(typeof api.example.insertTodo)["_args"]
|
||||||
|
> = (localStore, args) => {
|
||||||
|
const user = localStore.getQuery(api.example.getCurrentUser, undefined);
|
||||||
|
const todos = localStore.getQuery(api.example.listTodos, undefined);
|
||||||
|
if (!todos || !user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStore.setQuery(api.example.listTodos, {}, [
|
||||||
|
...todos,
|
||||||
|
{
|
||||||
|
_id: crypto.randomUUID() as Id<"todos">,
|
||||||
|
_creationTime: Date.now(),
|
||||||
|
userId: user._id,
|
||||||
|
text: args.text,
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const completeTodoOptimistic: OptimisticUpdate<
|
||||||
|
(typeof api.example.completeTodo)["_args"]
|
||||||
|
> = (localStore, args) => {
|
||||||
|
const todos = localStore.getQuery(api.example.listTodos, undefined);
|
||||||
|
if (!todos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const todo = todos.find((todo) => todo._id === args.todoId);
|
||||||
|
if (!todo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStore.setQuery(
|
||||||
|
api.example.listTodos,
|
||||||
|
{},
|
||||||
|
todos.map((todo) =>
|
||||||
|
todo._id === args.todoId ? { ...todo, completed: !todo.completed } : todo
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTodoOptimistic: OptimisticUpdate<
|
||||||
|
(typeof api.example.deleteTodo)["_args"]
|
||||||
|
> = (localStore, args) => {
|
||||||
|
const todos = localStore.getQuery(api.example.listTodos, undefined);
|
||||||
|
if (!todos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStore.setQuery(
|
||||||
|
api.example.listTodos,
|
||||||
|
{},
|
||||||
|
todos.filter((todo) => todo._id !== args.todoId)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"module": "./dist/esm/client/index.js",
|
"module": "./dist/esm/client/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polar-sh/checkout": "^0.1.10",
|
"@polar-sh/checkout": "^0.1.10",
|
||||||
"@polar-sh/sdk": "^0.28.0",
|
"@polar-sh/sdk": "^0.32.3",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"convex-helpers": "^0.1.63",
|
"convex-helpers": "^0.1.63",
|
||||||
"remeda": "^2.20.2",
|
"remeda": "^2.20.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user