Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tags #493

Merged
merged 1 commit into from
Sep 19, 2024
Merged

Tags #493

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/docs/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const SIDEBAR: Sidebar = {
{ text: "Control visibility", link: "en/courses/visibility" },
{ text: "Add content", link: "en/courses/add-content" },
{ text: "Manage sections", link: "en/products/section" },
{ text: "Invite customers", link: "en/products/invite-customers" },
],
"Digital downloads": [
{ text: "Introduction", link: "en/downloads/introduction" },
Expand Down
50 changes: 50 additions & 0 deletions apps/docs/src/pages/en/products/invite-customers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: Invite customers
description: Invite customers to a product
layout: ../../../layouts/MainLayout.astro
---

You can invite customers to your published products. This comes in handy in certain situations, such as:

- You collect the payments manually (or outside of CourseLit) and want to add customers manually afterward.
- You want to gift a product to a customer.

## Invite a customer

1. Go to the `Products` lobby and click on the three dots menu of the product you want to invite a customer to.

![Invite customer menu item](/assets/products/invite-customer-context-menu.png)

2. Click on the `Invite a customer` menu item from the three dots menu.

![Invite customer menu item selected](/assets/products/invite-customer-context-menu-invite.png)

3. On the subsequent screen, add the email of the customer.

![Invite customer screen](/assets/products/invite-customer-screen.png)

4. Optionally, add any tags you want to assign to the user.

> If a user with the same email already exists in the system, the tags you provide here will be added to their existing tags.

5. Click `Invite` to complete the invitation process.

![Invite customer success](/assets/products/invite-customer-success.png)

6. That's it! The invited user will receive an email at the email address you provided on this screen.

7. You can invite more users by repeating the same process.

## Invited customer's experience

The invited customer will receive an email stating they have been added to a product and can log in to access it.

![Invited customer's email](/assets/products/invite-customer-email.png)

Once logged in, they will see the products they have been added to in the `My Content` area. To access it, they can click on the `My Content` option from the `Session` button on the home page of the school.

![Invited customer's My Content area](/assets/products/invite-customer-my-content.png)

## Stuck somewhere?

We are always here for you. Come chat with us in our <a href="https://discord.com/invite/GR4bQsN" target="_blank">Discord</a> channel or send a tweet at <a href="https://twitter.com/courselit" target="_blank">@CourseLit</a>.
16 changes: 12 additions & 4 deletions apps/web/components/admin/products/new-customer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ function NewCustomer({
networkAction: loading,
}: NewCustomerProps) {
const [email, setEmail] = useState("");
const [tags, setTags] = useState([]);
const [tags, setTags] = useState<string[]>([]);
const [systemTags, setSystemTags] = useState<string[]>([]);

const getTags = useCallback(async () => {
const query = `
Expand All @@ -56,7 +57,7 @@ function NewCustomer({
dispatch(networkAction(true));
const response = await fetch.exec();
if (response.tags) {
setTags(response.tags);
setSystemTags(response.tags);
}
} catch (err) {
} finally {
Expand Down Expand Up @@ -103,6 +104,7 @@ function NewCustomer({
const response = await fetch.exec();
if (response.user) {
setEmail("");
setTags([]);
dispatch(
setAppMessage(
new AppMessage(
Expand All @@ -122,7 +124,9 @@ function NewCustomer({
<div className="flex flex-col gap-4">
<Breadcrumbs aria-label="breakcrumb">
<Link href="/dashboard/products/">Products</Link>
<Link href={`/dashboard/product/${courseId}/reports`}>Product</Link>
<Link href={`/dashboard/product/${courseId}/reports`}>
Product
</Link>
<p>{PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER}</p>
</Breadcrumbs>
<Section>
Expand All @@ -144,8 +148,12 @@ function NewCustomer({
<div className="flex flex-col gap-2">
<p>{USER_TAGS_SUBHEADER}</p>
<ComboBox
key={
JSON.stringify(systemTags) +
JSON.stringify(tags)
}
side="bottom"
options={tags}
options={systemTags}
selectedOptions={new Set(tags)}
onChange={(values: string[]) => setTags(values)}
/>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/admin/products/product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,15 @@ function Product({
{PRODUCT_TABLE_CONTEXT_MENU_EDIT_PAGE}
</Link>
</MenuItem>
<div className="flex w-full border-b border-slate-300"></div>
<div className="flex w-full border-b border-slate-200 my-1"></div>
<MenuItem>
<Link
href={`/dashboard/product/${product.courseId}/customer/new`}
>
{PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER}
</Link>
</MenuItem>
<div className="flex w-full border-b border-slate-300"></div>
<div className="flex w-full border-b border-slate-200 my-1"></div>
<MenuItem
component="dialog"
title={PRODUCT_TABLE_CONTEXT_MENU_DELETE_PRODUCT}
Expand Down
3 changes: 1 addition & 2 deletions apps/web/components/admin/users/details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,6 @@ const Details = ({ userId, address, dispatch }: DetailsProps) => {
<div className="flex items-center justify-between">
{SWITCH_ACCOUNT_ACTIVE}
<Switch
type="checkbox"
name="active"
checked={userData.active}
onChange={(value) => toggleActiveState(value)}
/>
Expand All @@ -255,6 +253,7 @@ const Details = ({ userId, address, dispatch }: DetailsProps) => {
options={tags}
selectedOptions={new Set(userData.tags)}
onChange={updateTags}
side="bottom"
/>
</div>
</Section>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/config/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export const responses = {
mandatory_tags_missing: "Mandatory tags are missing",
cannot_delete_last_email: "Cannot delete the last email in the sequence",
invalid_drip_email: "Drip email needs a subject and a body",
cannot_invite_to_unpublished_product:
"Cannot invite customers to an unpublished product",

// api responses
digital_download_no_files:
Expand Down
16 changes: 14 additions & 2 deletions apps/web/graphql/users/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ export const inviteCustomer = async (
}

const course = await getCourseOrThrow(undefined, ctx, id);
if (!course.published) {
throw new Error(responses.cannot_invite_to_unpublished_product);
}

const sanitizedEmail = (email as string).toLowerCase();
let user = await UserModel.findOne({
email: sanitizedEmail,
Expand All @@ -176,6 +180,14 @@ export const inviteCustomer = async (
invited: true,
});
}

if (tags.length) {
user = await updateUser(
{ id: user._id, tags: [...user.tags, ...tags] },
ctx,
);
}

if (
!user.purchases.some(
(purchase) => purchase.courseId === course.courseId,
Expand All @@ -193,15 +205,15 @@ export const inviteCustomer = async (

await send({
to: [user.email],
subject: `You have been invited in ${course.title}`,
subject: `You have been invited to ${course.title}`,
body: emailBody,
});
} catch (error) {
// eslint-disable-next-line no-console
console.log("error", error);
}
}
user = await updateUser({ id: user._id, tags }, ctx);

return user;
};

Expand Down
2 changes: 1 addition & 1 deletion apps/web/graphql/users/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
GraphQLString,
GraphQLList,
GraphQLNonNull,
GraphQLID
GraphQLID,
} from "graphql";
import types from "./types";
import {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const nextConfig = {
],
experimental: {
serverComponentsExternalPackages: ["pug"],
},
},
};

module.exports = nextConfig;
9 changes: 3 additions & 6 deletions apps/web/pages/dashboard/product/[id]/customer/new.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
"use client"
"use client";

import React from "react";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER } from "@ui-config/strings";

const BaseLayout = dynamic(
() => import("@components/admin/base-layout"),
);
const BaseLayout = dynamic(() => import("@components/admin/base-layout"));

const NewCustomer = dynamic(
() => import("@components/admin/products/new-customer"),
);


export default function New() {
const router = useRouter();
const { id } = router.query;
return (
<BaseLayout title={PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER}>
<NewCustomer courseId={id as string}/>
<NewCustomer courseId={id as string} />
</BaseLayout>
);
}
5 changes: 3 additions & 2 deletions packages/components-library/src/combo-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function ComboBox({
useState<Set<string>>(selectedOptions);
const [text, setText] = useState("");
const [internalOpen, setInternalOpen] = useState(false);
const outlineClass = `flex flex-wrap max-w-[220px] min-w-[220px] min-h-[36px] gap-2 border border-slate-300 hover:border-slate-400 rounded py-1 px-2 outline-none focus:border-slate-600 disabled:pointer-events-none overflow-y-auto ${className}`;
const outlineClass = `flex flex-wrap min-w-[220px] min-h-[36px] gap-2 border border-slate-300 hover:border-slate-400 rounded py-1 px-2 outline-none focus:border-slate-600 disabled:pointer-events-none overflow-y-auto ${className}`;

const onOptionAdd = () => {
internalSelectedOptions.add(text);
Expand Down Expand Up @@ -95,11 +95,12 @@ export default function ComboBox({
<Form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
onOptionAdd();
}}
>
<input
className="outline-none max-w-[100px]"
className="outline-none"
type="text"
value={text}
autoFocus
Expand Down
Loading