mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-06 04:20:58 +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 { api } from "../convex/_generated/api";
|
||||
import { CheckoutLink, CustomerPortalLink } from "../../src/react";
|
||||
import {
|
||||
insertTodoOptimistic,
|
||||
completeTodoOptimistic,
|
||||
deleteTodoOptimistic,
|
||||
} from "@/optimistic";
|
||||
|
||||
export default function TodoList() {
|
||||
const user = useQuery(api.example.getCurrentUser);
|
||||
const todos = useQuery(api.example.listTodos);
|
||||
const products = useQuery(api.example.getConfiguredProducts);
|
||||
const insertTodo = useMutation(api.example.insertTodo);
|
||||
const completeTodo = useMutation(api.example.completeTodo);
|
||||
const deleteTodo = useMutation(api.example.deleteTodo);
|
||||
const insertTodo = useMutation(api.example.insertTodo).withOptimisticUpdate(
|
||||
insertTodoOptimistic
|
||||
);
|
||||
const completeTodo = useMutation(
|
||||
api.example.completeTodo
|
||||
).withOptimisticUpdate(completeTodoOptimistic);
|
||||
const deleteTodo = useMutation(api.example.deleteTodo).withOptimisticUpdate(
|
||||
deleteTodoOptimistic
|
||||
);
|
||||
const cancelSubscription = useAction(api.example.cancelCurrentSubscription);
|
||||
const changeSubscription = useAction(api.example.changeCurrentSubscription);
|
||||
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">
|
||||
Todo List
|
||||
</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">
|
||||
<Input
|
||||
type="text"
|
||||
@@ -169,6 +188,14 @@ export default function TodoList() {
|
||||
{user.subscription.recurringInterval}
|
||||
</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>
|
||||
|
||||
@@ -185,17 +212,25 @@ export default function TodoList() {
|
||||
<div>
|
||||
<h4 className="font-medium">Premium</h4>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
${(premiumMonthly.prices[0].priceAmount ?? 0) / 100}
|
||||
/month
|
||||
</p>
|
||||
{premiumYearly && (
|
||||
<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">
|
||||
$
|
||||
{(premiumYearly.prices[0].priceAmount ?? 0) / 100}
|
||||
/year
|
||||
</p>
|
||||
)}
|
||||
{(premiumMonthly.prices[0].priceAmount ?? 0) /
|
||||
100}
|
||||
/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>
|
||||
{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"
|
||||
embed={false}
|
||||
>
|
||||
Upgrade to Premium
|
||||
Upgrade to Premium (redirect)
|
||||
</CheckoutLink>
|
||||
) : (
|
||||
<Button
|
||||
@@ -232,20 +267,25 @@ export default function TodoList() {
|
||||
<div>
|
||||
<h4 className="font-medium">Premium Plus</h4>
|
||||
<div className="space-y-1">
|
||||
<p 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">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
$
|
||||
{(premiumPlusYearly.prices[0].priceAmount ?? 0) /
|
||||
{(premiumPlusMonthly.prices[0].priceAmount ?? 0) /
|
||||
100}
|
||||
/year
|
||||
</p>
|
||||
)}
|
||||
/month
|
||||
</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>
|
||||
{user?.subscription?.productId !==
|
||||
@@ -262,7 +302,7 @@ export default function TodoList() {
|
||||
].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"
|
||||
>
|
||||
Upgrade to Premium Plus
|
||||
Upgrade to Premium Plus (modal)
|
||||
</CheckoutLink>
|
||||
) : (
|
||||
<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",
|
||||
"dependencies": {
|
||||
"@polar-sh/checkout": "^0.1.10",
|
||||
"@polar-sh/sdk": "^0.28.0",
|
||||
"@polar-sh/sdk": "^0.32.3",
|
||||
"buffer": "^6.0.3",
|
||||
"convex-helpers": "^0.1.63",
|
||||
"remeda": "^2.20.2",
|
||||
|
||||
Reference in New Issue
Block a user