Skip to content
Draft
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
10 changes: 5 additions & 5 deletions apps/web/.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# For persisting data
# DB_CONNECTION_STRING=mongodb://your_connection_string
DB_CONNECTION_STRING=mongodb://localhost:27017/courselit

# For sending emails
# EMAIL_USER=email_user
Expand All @@ -12,16 +12,16 @@
# MEDIALIT_APIKEY=medialit_apikey
# MEDIALIT_SERVER=medialit_server

# For carrying out tasks asynchronously.
# For carrying out tasks asynchronously.
#
# Uncomment the next line if you have started the queue server.
# # QUEUE_SERVER=http://localhost:4000
QUEUE_SERVER=http://localhost:4000

# App secrets
# AUTH_SECRET=long_random_string
AUTH_SECRET=a-long-random-string-for-testing

# For setting up the admin user when the app first boots up
# [email protected]

# Sequence settings
# SEQUENCE_DELAY_BETWEEN_MAILS = 86400000 # 1 day in milliseconds
# SEQUENCE_DELAY_BETWEEN_MAILS = 86400000 # 1 day in milliseconds
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use client';

import { gql, useQuery, useMutation } from '@apollo/client';
import { Button, Card, CardBody, CardHeader, Checkbox, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Snippet } from '@nextui-org/react';
import { getAbsoluteUrl } from '@/__shared__/utils/url';
import { useEffect, useState } from 'react';

interface SitemapItem {
loc: string;
lastmod?: string;
}

interface Page {
pageId: string;
title: string;
}

const GET_SITEMAP = gql`
query Sitemap($domain: String!) {
sitemap(domain: $domain) {
_id
items {
loc
lastmod
}
publishLatestBlogs
}
}
`;

const UPDATE_SITEMAP = gql`
mutation UpdateSitemap($domain: String!, $items: [SitemapItemInput!]!, $publishLatestBlogs: Boolean) {
updateSitemap(domain: $domain, items: $items, publishLatestBlogs: $publishLatestBlogs) {
_id
}
}
`;

const GET_PAGES = gql`
query pages {
getPages(type: site) {
pageId
title
}
}
`;

export default function SitemapEditor() {
const [domain, setDomain] = useState('');
const [items, setItems] = useState<SitemapItem[]>([]);
const [publishLatestBlogs, setPublishLatestBlogs] = useState(false);

useEffect(() => {
setDomain(window.location.hostname);
}, []);

const { data, loading, refetch } = useQuery(GET_SITEMAP, {
variables: { domain },
skip: !domain,
});

useEffect(() => {
if (data?.sitemap) {
setItems(data.sitemap.items.map((item: SitemapItem) => ({ ...item })));
setPublishLatestBlogs(data.sitemap.publishLatestBlogs);
}
}, [data]);

const { data: pagesData } = useQuery(GET_PAGES);

const [updateSitemap] = useMutation(UPDATE_SITEMAP);

const handleAddItem = (page: Page | undefined) => {
if (page) {
setItems([...items, { loc: `/${page.pageId}`, lastmod: new Date().toISOString() }]);
}
};

const handleRemoveItem = (index: number) => {
const newItems = [...items];
newItems.splice(index, 1);
setItems(newItems);
};

const handleSaveChanges = async () => {
await updateSitemap({
variables: {
domain,
items: items.map(item => ({ loc: item.loc, lastmod: item.lastmod })),
publishLatestBlogs,
},
});
refetch();
};

if (loading) return <p>Loading...</p>;

return (
<Card>
<CardHeader>Sitemap Editor</CardHeader>
<CardBody>
<Snippet>
{getAbsoluteUrl('/sitemap')}
</Snippet>

<div className="mt-4">
{items.map((item, index) => (
<div key={index} className="flex items-center justify-between mb-2">
<Input
value={item.loc}
onChange={(e) => {
const newItems = [...items];
newItems[index].loc = e.target.value;
setItems(newItems);
}}
/>
<Button color="danger" onClick={() => handleRemoveItem(index)}>Remove</Button>
</div>
))}
</div>

<div className="mt-4">
<Dropdown>
<DropdownTrigger>
<Button>Add Page</Button>
</DropdownTrigger>
<DropdownMenu onAction={(key) => handleAddItem(pagesData?.getPages.find((page: Page) => page.pageId === key))}>
{pagesData?.getPages.map((page: Page) => (
<DropdownItem key={page.pageId}>{page.title}</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>

<div className="mt-4">
<Checkbox
isSelected={publishLatestBlogs}
onValueChange={setPublishLatestBlogs}
>
Publish latest blogs to sitemap automatically
</Checkbox>
</div>

<div className="mt-t-4">
<Button color="primary" onClick={handleSaveChanges}>Save Changes</Button>
</div>
</CardBody>
</Card>
);
}
40 changes: 40 additions & 0 deletions apps/web/app/sitemap/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getSitemap } from '../../graphql/sitemap/logic';
import { headers } from 'next/headers';
import Page from '../../models/Page';

export async function GET() {
const headersList = headers();
const domain = headersList.get('host') || '';
const sitemap = await getSitemap(domain);
const blogs = sitemap.publishLatestBlogs ? await Page.find({ domain, type: 'blogPage' }) : [];

const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemap.items
.map(
(item) => `
<url>
<loc>${new URL(item.loc, `https://${domain}`).href}</loc>
${item.lastmod ? `<lastmod>${item.lastmod}</lastmod>` : ''}
</url>
`
)
.join('')}
${blogs
.map(
(blog) => `
<url>
<loc>${new URL(`/blog/${blog.slug}`, `https://${domain}`).href}</loc>
<lastmod>${new Date(blog.updatedAt).toISOString()}</lastmod>
</url>
`
)
.join('')}
</urlset>`;

return new Response(sitemapXml, {
headers: {
'Content-Type': 'application/xml',
},
});
}
6 changes: 6 additions & 0 deletions apps/web/components/admin/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { Copy, Info } from "lucide-react";
import { Input } from "@components/ui/input";
import Resources from "@components/resources";
import { AddressContext } from "@components/contexts";
import SitemapEditor from "../../../app/(with-contexts)/dashboard/(sidebar)/settings/sitemap/page";

const {
PAYMENT_METHOD_PAYPAL,
Expand Down Expand Up @@ -125,6 +126,7 @@ const Settings = (props: SettingsProps) => {
SITE_MAILS_HEADER,
SITE_CUSTOMISATIONS_SETTING_HEADER,
SITE_APIKEYS_SETTING_HEADER,
"Sitemap",
].includes(props.selectedTab)
? props.selectedTab
: SITE_SETTINGS_SECTION_GENERAL;
Expand Down Expand Up @@ -696,6 +698,7 @@ const Settings = (props: SettingsProps) => {
SITE_MAILS_HEADER,
SITE_CUSTOMISATIONS_SETTING_HEADER,
SITE_APIKEYS_SETTING_HEADER,
"Sitemap",
];

const copyToClipboard = (text: string) => {
Expand Down Expand Up @@ -1242,6 +1245,9 @@ const Settings = (props: SettingsProps) => {
]}
/>
</div>
<div>
<SitemapEditor />
</div>
</Tabbs>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions apps/web/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import communities from "./communities";
import paymentplans from "./paymentplans";
import notifications from "./notifications";
import themes from "./themes";
import sitemap from "./sitemap";

const schema = new graphql.GraphQLSchema({
query: new graphql.GraphQLObjectType({
name: "RootQuery",
fields: {
...sitemap.queries,
...users.queries,
...lessons.queries,
...courses.queries,
Expand All @@ -36,6 +38,7 @@ const schema = new graphql.GraphQLSchema({
mutation: new graphql.GraphQLObjectType({
name: "RootMutation",
fields: {
...sitemap.mutations,
...users.mutations,
...lessons.mutations,
...courses.mutations,
Expand Down
7 changes: 7 additions & 0 deletions apps/web/graphql/sitemap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import queries from "./query";
import mutations from "./mutation";

export default {
queries,
mutations,
};
18 changes: 18 additions & 0 deletions apps/web/graphql/sitemap/logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Sitemap from '../../models/Sitemap';

export const getSitemap = async (domain: string) => {
let sitemap = await Sitemap.findOne({ domain });
if (!sitemap) {
sitemap = await Sitemap.create({ domain });
}
return sitemap;
};

export const updateSitemap = async (domain: string, items: any[], publishLatestBlogs: boolean) => {
const sitemap = await Sitemap.findOneAndUpdate(
{ domain },
{ items, publishLatestBlogs },
{ new: true, upsert: true }
);
return sitemap;
};
24 changes: 24 additions & 0 deletions apps/web/graphql/sitemap/mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { GraphQLString, GraphQLList, GraphQLBoolean, GraphQLNonNull } from 'graphql';
import { updateSitemap } from './logic';
import types from './types';

const mutations = {
updateSitemap: {
type: types.Sitemap,
args: {
domain: { type: new GraphQLNonNull(GraphQLString) },
items: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(types.SitemapItemInput))) },
publishLatestBlogs: { type: GraphQLBoolean },
},
resolve: (
_: any,
{
domain,
items,
publishLatestBlogs,
}: { domain: string; items: any[]; publishLatestBlogs: boolean }
) => updateSitemap(domain, items, publishLatestBlogs),
},
};

export default mutations;
17 changes: 17 additions & 0 deletions apps/web/graphql/sitemap/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { getSitemap } from './logic';
import types from './types';

const queries = {
sitemap: {
type: types.Sitemap,
args: {
domain: {
type: new GraphQLNonNull(GraphQLString),
},
},
resolve: (_: any, { domain }: { domain: string }) => getSitemap(domain),
},
};

export default queries;
39 changes: 39 additions & 0 deletions apps/web/graphql/sitemap/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
GraphQLObjectType,
GraphQLString,
GraphQLList,
GraphQLBoolean,
GraphQLInputObjectType,
GraphQLNonNull,
} from 'graphql';

const SitemapItem = new GraphQLObjectType({
name: 'SitemapItem',
fields: {
loc: { type: new GraphQLNonNull(GraphQLString) },
lastmod: { type: GraphQLString },
},
});

const Sitemap = new GraphQLObjectType({
name: 'Sitemap',
fields: {
_id: { type: new GraphQLNonNull(GraphQLString) },
domain: { type: new GraphQLNonNull(GraphQLString) },
items: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SitemapItem))) },
publishLatestBlogs: { type: GraphQLBoolean },
},
});

const SitemapItemInput = new GraphQLInputObjectType({
name: 'SitemapItemInput',
fields: {
loc: { type: new GraphQLNonNull(GraphQLString) },
lastmod: { type: GraphQLString },
},
});

export default {
Sitemap,
SitemapItemInput,
};
Loading
Loading