87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/dashboard/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
3 | import { VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | })
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 |
26 | {children}
27 |
28 |
29 | ))
30 |
31 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
32 |
33 | const ToggleGroupItem = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef &
36 | VariantProps
37 | >(({ className, children, variant, size, ...props }, ref) => {
38 | const context = React.useContext(ToggleGroupContext)
39 |
40 | return (
41 |
52 | {children}
53 |
54 | )
55 | })
56 |
57 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
58 |
59 | export { ToggleGroup, ToggleGroupItem }
60 |
--------------------------------------------------------------------------------
/dashboard/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-9 px-3",
18 | sm: "h-8 px-2",
19 | lg: "h-10 px-3",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName
42 |
43 | export { Toggle, toggleVariants }
44 |
--------------------------------------------------------------------------------
/dashboard/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/dashboard/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --sidebar-width: 16rem;
7 | }
8 |
9 | @layer base {
10 | :root {
11 | --background: 0 0% 100%;
12 | --foreground: 222.2 84% 4.9%;
13 | --card: 0 0% 100%;
14 | --card-foreground: 222.2 84% 4.9%;
15 | --popover: 0 0% 100%;
16 | --popover-foreground: 222.2 84% 4.9%;
17 | --primary: 222.2 47.4% 11.2%;
18 | --primary-foreground: 210 40% 98%;
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 | --muted: 210 40% 96.1%;
22 | --muted-foreground: 215.4 16.3% 46.9%;
23 | --accent: 210 40% 96.1%;
24 | --accent-foreground: 222.2 47.4% 11.2%;
25 | --destructive: 0 84.2% 60.2%;
26 | --destructive-foreground: 210 40% 98%;
27 | --border: 214.3 31.8% 91.4%;
28 | --input: 214.3 31.8% 91.4%;
29 | --ring: 222.2 84% 4.9%;
30 | --radius: 0.5rem;
31 |
32 | --display-font: 'Cal Sans', system-ui;
33 |
34 | --highlight-default: #ffffff;
35 | --highlight-purple: #f6f3f8;
36 | --highlight-red: #fdebeb;
37 | --highlight-yellow: #fbf4a2;
38 | --highlight-blue: #c1ecf9;
39 | --highlight-green: #acf79f;
40 | --highlight-orange: #faebdd;
41 | --highlight-pink: #faf1f5;
42 | --highlight-gray: #f1f1ef;
43 | }
44 |
45 | .dark {
46 | --background: 222.2 84% 4.9%;
47 | --foreground: 210 40% 98%;
48 | --card: 222.2 84% 4.9%;
49 | --card-foreground: 210 40% 98%;
50 | --popover: 222.2 84% 4.9%;
51 | --popover-foreground: 210 40% 98%;
52 | --primary: 210 40% 98%;
53 | --primary-foreground: 222.2 47.4% 11.2%;
54 | --secondary: 217.2 32.6% 17.5%;
55 | --secondary-foreground: 210 40% 98%;
56 | --muted: 217.2 32.6% 17.5%;
57 | --muted-foreground: 215 20.2% 65.1%;
58 | --accent: 217.2 32.6% 17.5%;
59 | --accent-foreground: 210 40% 98%;
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 | --border: 217.2 32.6% 17.5%;
63 | --input: 217.2 32.6% 17.5%;
64 | --ring: 212.7 26.8% 83.9;
65 |
66 | --highlight-default: #000000;
67 | --highlight-purple: #3f2c4b;
68 | --highlight-red: #5c1a1a;
69 | --highlight-yellow: #5c4b1a;
70 | --highlight-blue: #1a3d5c;
71 | --highlight-green: #1a5c20;
72 | --highlight-orange: #5c3a1a;
73 | --highlight-pink: #5c1a3a;
74 | --highlight-gray: #3a3a3a;
75 | }
76 | }
77 |
78 |
79 | @layer base {
80 | * {
81 | @apply border-border;
82 | }
83 |
84 | body {
85 | @apply bg-background text-foreground;
86 | }
87 | }
88 |
89 | .cal-sans {
90 | font-family: var(--display-font);
91 | }
--------------------------------------------------------------------------------
/dashboard/src/lib/UserProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useFrappeAuth, useSWRConfig } from 'frappe-react-sdk'
2 | import { FC, PropsWithChildren, useContext } from 'react'
3 | import { createContext } from 'react'
4 |
5 | interface UserContextProps {
6 | isLoading: boolean,
7 | currentUser: string,
8 | login: (username: string, password: string) => Promise,
9 | logout: () => Promise,
10 | updateCurrentUser: VoidFunction,
11 | }
12 |
13 | export const UserContext = createContext({
14 | currentUser: '',
15 | isLoading: false,
16 | login: () => Promise.resolve(),
17 | logout: () => Promise.resolve(),
18 | updateCurrentUser: () => { },
19 | })
20 |
21 | export const UserProvider: FC = ({ children }) => {
22 |
23 | const { mutate } = useSWRConfig()
24 | const { login, logout, currentUser, updateCurrentUser, isLoading } = useFrappeAuth()
25 |
26 | const handleLogout = async () => {
27 | localStorage.removeItem('ravenLastChannel')
28 | return logout()
29 | .then(() => {
30 | //Clear cache on logout
31 | return mutate(() => true, undefined, false)
32 | })
33 | .then(() => {
34 | //Reload the page so that the boot info is fetched again
35 | const URL = import.meta.env.VITE_BASE_NAME ? `${import.meta.env.VITE_BASE_NAME}` : ``
36 | window.location.replace(`/login?redirect-to=${URL}/`)
37 | // window.location.reload()
38 | })
39 | }
40 |
41 | const handleLogin = async (username: string, password: string) => {
42 | return login(username, password)
43 | .then(() => {
44 | //Reload the page so that the boot info is fetched again
45 | const URL = import.meta.env.VITE_BASE_NAME ? `/${import.meta.env.VITE_BASE_NAME}` : ``
46 | window.location.replace(`${URL}/`)
47 | })
48 | }
49 | return (
50 |
51 | {children}
52 |
53 | )
54 | }
55 |
56 | export const useUser = () => {
57 | const { currentUser } = useContext(UserContext)
58 |
59 | return currentUser
60 | }
--------------------------------------------------------------------------------
/dashboard/src/lib/dates.ts:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 |
3 | /**
4 | * Converts a Frappe date to a readable time ago string
5 | * @param date A frappe date string in the format YYYY-MM-DD
6 | * @param withoutSuffix remove the suffix from the time ago string
7 | * @returns
8 | */
9 | export const convertFrappeDateStringToTimeAgo = (date?: string, withoutSuffix?: boolean) => {
10 | if (date) {
11 | const userDate = moment(date)
12 | return userDate.fromNow(withoutSuffix)
13 | }
14 | return ''
15 | }
--------------------------------------------------------------------------------
/dashboard/src/lib/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useLocalStorage = (
4 | key: string,
5 | initialValue: T
6 | // eslint-disable-next-line no-unused-vars
7 | ): [T, (value: T) => void] => {
8 | const [storedValue, setStoredValue] = useState(initialValue);
9 |
10 | useEffect(() => {
11 | // Retrieve from localStorage
12 | const item = window.localStorage.getItem(key);
13 | if (item) {
14 | setStoredValue(JSON.parse(item));
15 | }
16 | }, [key]);
17 |
18 | const setValue = (value: T) => {
19 | // Save state
20 | setStoredValue(value);
21 | // Save to localStorage
22 | window.localStorage.setItem(key, JSON.stringify(value));
23 | };
24 | return [storedValue, setValue];
25 | };
26 |
27 | export default useLocalStorage;
--------------------------------------------------------------------------------
/dashboard/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 |
9 | export function isValidUrl(url: string) {
10 | try {
11 | new URL(url);
12 | return true;
13 | } catch (e) {
14 | return false;
15 | }
16 | }
17 |
18 | export function getUrlFromString(str: string) {
19 | if (isValidUrl(str)) return str;
20 | try {
21 | if (str.includes(".") && !str.includes(" ")) {
22 | return new URL(`https://${str}`).toString();
23 | }
24 | } catch (e) {
25 | return null;
26 | }
27 | }
--------------------------------------------------------------------------------
/dashboard/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/dashboard/src/pages/Audience.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBanner } from "@/components/layout/ErrorBanner"
2 | import { PageHeader, PageHeading } from "@/components/layout/PageHeader"
3 | import { TableLoader } from "@/components/layout/TableLoader"
4 | import { Button } from "@/components/ui/button"
5 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
6 | import { convertFrappeDateStringToTimeAgo } from "@/lib/dates"
7 | import { useFrappeGetDocList } from "frappe-react-sdk"
8 | import { Import, Plus } from "lucide-react"
9 |
10 | export const Audience = () => {
11 | return (
12 |
13 |
14 | Audience
15 |
16 |
20 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | const AudienceList = () => {
32 |
33 | const { data, isLoading, error } = useFrappeGetDocList('Email Group', {
34 | fields: ['name', 'title', 'modified', 'total_subscribers', 'creation', 'owner'],
35 | orderBy: {
36 | field: 'modified',
37 | order: 'desc'
38 | }
39 | })
40 |
41 | //TODO: Pagination and search/filters
42 |
43 | if (error) {
44 | return
45 | }
46 |
47 | return
48 |
49 |
50 | Name
51 | Subscribers
52 | Created
53 |
54 |
55 | {isLoading && }
56 | {data &&
57 | {data.map(doc =>
58 | {doc.title}
59 | {(doc.total_subscribers ?? 0).toLocaleString()}
60 | {convertFrappeDateStringToTimeAgo(doc.creation)}
61 | )}
62 | }
63 |
64 |
65 | }
--------------------------------------------------------------------------------
/dashboard/src/pages/Authors.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBanner } from "@/components/layout/ErrorBanner"
2 | import { PageHeader, PageHeading } from "@/components/layout/PageHeader"
3 | import { TableLoader } from "@/components/layout/TableLoader"
4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
5 | import { Badge } from "@/components/ui/badge"
6 | import { Button } from "@/components/ui/button"
7 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
8 | import { convertFrappeDateStringToTimeAgo } from "@/lib/dates"
9 | import { useFrappeGetDocList } from "frappe-react-sdk"
10 | import { Plus } from "lucide-react"
11 |
12 | export const Authors = () => {
13 | return (
14 |
15 |
16 | Authors
17 |
18 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 | const getInitials = (name?: string) => {
29 | if (!name) return ''
30 | const [firstName, lastName] = name.split(' ')
31 | return firstName[0] + (lastName?.[0] ?? '')
32 | }
33 | const AuthorsList = () => {
34 |
35 | const { data, isLoading, error } = useFrappeGetDocList('Blogger', {
36 | fields: ['name', 'full_name', 'modified', 'disabled', 'creation', 'owner', 'avatar', 'short_name'],
37 | orderBy: {
38 | field: 'modified',
39 | order: 'desc'
40 | }
41 | })
42 |
43 | //TODO: Pagination and search/filters
44 |
45 | if (error) {
46 | return
47 | }
48 |
49 | return
50 |
51 |
52 | Name
53 | Short name
54 | Status
55 | Created
56 |
57 |
58 | {isLoading && }
59 | {data &&
60 | {data.map(doc =>
61 |
62 |
63 |
64 |
65 | {getInitials(doc.full_name)}
66 |
67 | {doc.full_name}
68 |
69 |
70 |
71 | {doc.short_name}
72 | {doc.disabled ? Disabled : Enabled}
73 | {convertFrappeDateStringToTimeAgo(doc.creation)}
74 | )}
75 | }
76 |
77 |
78 | }
--------------------------------------------------------------------------------
/dashboard/src/pages/Categories.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBanner } from "@/components/layout/ErrorBanner"
2 | import { PageHeader, PageHeading } from "@/components/layout/PageHeader"
3 | import { TableLoader } from "@/components/layout/TableLoader"
4 | import { Badge } from "@/components/ui/badge"
5 | import { Button } from "@/components/ui/button"
6 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
7 | import { convertFrappeDateStringToTimeAgo } from "@/lib/dates"
8 | import { useFrappeGetDocList } from "frappe-react-sdk"
9 | import { Plus } from "lucide-react"
10 |
11 | export const Categories = () => {
12 | return (
13 |
14 |
15 | Categories
16 |
17 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | const CategoriesList = () => {
29 |
30 | const { data, isLoading, error } = useFrappeGetDocList('Blog Category', {
31 | fields: ['name', 'title', 'modified', 'published', 'creation', 'owner'],
32 | orderBy: {
33 | field: 'modified',
34 | order: 'desc'
35 | }
36 | })
37 |
38 | //TODO: Pagination and search/filters
39 |
40 | if (error) {
41 | return
42 | }
43 |
44 | return
45 |
46 |
47 | Name
48 | Status
49 | Created
50 |
51 |
52 | {isLoading && }
53 | {data &&
54 | {data.map(doc =>
55 | {doc.title}
56 | {doc.published ? Published : Not Published}
57 | {convertFrappeDateStringToTimeAgo(doc.creation)}
58 | )}
59 | }
60 |
61 |
62 | }
--------------------------------------------------------------------------------
/dashboard/src/pages/NewsletterEditor.tsx:
--------------------------------------------------------------------------------
1 | import Editor from '@/components/features/editor'
2 | import { NewsletterHeader } from '@/components/features/newsletter/NewsletterHeader'
3 | import { ErrorBanner } from '@/components/layout/ErrorBanner'
4 | import { FullPageLoader } from '@/components/ui/full-page-loader'
5 | import { Newsletter } from '@/types/Newsletter'
6 | import { useFrappeGetDoc } from 'frappe-react-sdk'
7 | import { FormProvider, useForm } from 'react-hook-form'
8 | import { useParams } from 'react-router-dom'
9 | import { Editor as EditorClass } from "@tiptap/core";
10 |
11 | export const NewsletterEditor = () => {
12 |
13 | const { id } = useParams<{ id: string }>()
14 |
15 | const { data, isLoading, error } = useFrappeGetDoc('Newsletter', id)
16 | return (
17 |
18 |
19 | {isLoading && }
20 | {data && }
21 |
22 | )
23 | }
24 |
25 |
26 | const NewsletterEditorContent = ({ newsletter }: { newsletter: Newsletter }) => {
27 |
28 | const methods = useForm()
29 |
30 | const onSubmit = (data: Newsletter) => {
31 | console.log(data)
32 | }
33 |
34 | const onEditorUpdate = (editor?: EditorClass) => {
35 |
36 | console.log(editor?.getHTML())
37 |
38 | }
39 | return
53 | }
--------------------------------------------------------------------------------
/dashboard/src/pages/Newsletters.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBanner } from "@/components/layout/ErrorBanner"
2 | import { PageHeader, PageHeading } from "@/components/layout/PageHeader"
3 | import { TableLoader } from "@/components/layout/TableLoader"
4 | import { Badge } from "@/components/ui/badge"
5 | import { Button } from "@/components/ui/button"
6 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
7 | import { convertFrappeDateStringToTimeAgo } from "@/lib/dates"
8 | import { useFrappeGetDocList } from "frappe-react-sdk"
9 | import { Plus } from "lucide-react"
10 | import { Link } from "react-router-dom"
11 |
12 | export const Newsletters = () => {
13 | return (
14 |
15 |
16 | Newsletters
17 |
18 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | const NewslettersList = () => {
30 |
31 | const { data, isLoading, error } = useFrappeGetDocList('Newsletter', {
32 | fields: ['name', 'subject', 'modified', 'email_sent', 'creation', 'owner'],
33 | orderBy: {
34 | field: 'modified',
35 | order: 'desc'
36 | }
37 | })
38 |
39 | //TODO: Pagination and search/filters
40 |
41 | if (error) {
42 | return
43 | }
44 |
45 | return
46 |
47 |
48 | Name
49 | Status
50 | Created
51 |
52 |
53 | {isLoading && }
54 | {data &&
55 | {data.map(doc =>
56 | {doc.subject}
57 | {doc.email_sent ? Sent : Draft}
58 | {convertFrappeDateStringToTimeAgo(doc.creation)}
59 | )}
60 | }
61 |
62 |
63 | }
--------------------------------------------------------------------------------
/dashboard/src/pages/Posts.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBanner } from "@/components/layout/ErrorBanner"
2 | import { PageHeader, PageHeading } from "@/components/layout/PageHeader"
3 | import { TableLoader } from "@/components/layout/TableLoader"
4 | import { Badge } from "@/components/ui/badge"
5 | import { Button } from "@/components/ui/button"
6 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
7 | import { convertFrappeDateStringToTimeAgo } from "@/lib/dates"
8 | import { useFrappeGetDocList } from "frappe-react-sdk"
9 | import { Plus } from "lucide-react"
10 |
11 | export const Posts = () => {
12 | return (
13 |
14 |
15 | Posts
16 |
17 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | const PostsList = () => {
29 |
30 | const { data, isLoading, error } = useFrappeGetDocList('Blog Post', {
31 | fields: ['name', 'title', 'blog_category', 'blogger', 'modified', 'published', 'creation', 'owner'],
32 | orderBy: {
33 | field: 'modified',
34 | order: 'desc'
35 | }
36 | })
37 |
38 | //TODO: Pagination and search/filters
39 |
40 | if (error) {
41 | return
42 | }
43 |
44 | return
45 |
46 |
47 | Name
48 | Category
49 | Author
50 | Status
51 | Created
52 |
53 |
54 | {isLoading && }
55 | {data &&
56 | {data.map(doc =>
57 | {doc.title}
58 | {doc.blog_category}
59 | {doc.blogger}
60 | {doc.published ? Published : Not Published}
61 | {convertFrappeDateStringToTimeAgo(doc.creation)}
62 | )}
63 | }
64 |
65 |
66 | }
--------------------------------------------------------------------------------
/dashboard/src/types/Newsletter.ts:
--------------------------------------------------------------------------------
1 | export interface Newsletter {
2 | subject: string;
3 | sender_name: string;
4 | sender_email: string;
5 | message_html: string;
6 | email_sent: 0 | 1
7 | }
--------------------------------------------------------------------------------
/dashboard/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/dashboard/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/dashboard/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": [
6 | "ES2020",
7 | "DOM",
8 | "DOM.Iterable"
9 | ],
10 | "module": "ESNext",
11 | "skipLibCheck": true,
12 | /* Bundler mode */
13 | "moduleResolution": "bundler",
14 | "allowImportingTsExtensions": true,
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "baseUrl": ".",
25 | "paths": {
26 | "@/*": [
27 | "./src/*"
28 | ]
29 | }
30 | },
31 | "include": [
32 | "src"
33 | ],
34 | "references": [
35 | {
36 | "path": "./tsconfig.node.json"
37 | }
38 | ]
39 | }
--------------------------------------------------------------------------------
/dashboard/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/dashboard/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { defineConfig } from 'vite';
3 | import react from '@vitejs/plugin-react'
4 | import proxyOptions from './proxyOptions';
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [react()],
9 | server: {
10 | port: 8080,
11 | proxy: proxyOptions
12 | },
13 | resolve: {
14 | alias: {
15 | '@': path.resolve(__dirname, 'src')
16 | }
17 | },
18 | build: {
19 | outDir: '../mailrun/public/mailrun',
20 | emptyOutDir: true,
21 | target: 'es2015',
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/mailrun/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | __version__ = '0.0.1'
3 |
4 |
--------------------------------------------------------------------------------
/mailrun/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Commit-Company/mailrun/6a896b1ba3e0fa71aa70ef69fdba48ba8c03d7d4/mailrun/config/__init__.py
--------------------------------------------------------------------------------
/mailrun/hooks.py:
--------------------------------------------------------------------------------
1 | app_name = "mailrun"
2 | app_title = "MailRun"
3 | app_publisher = "Nikhil Kothari"
4 | app_description = "Frappe app to manage newsletters"
5 | app_email = "nik.kothari22@live.com"
6 | app_license = "agpl-3.0"
7 | # required_apps = []
8 |
9 | # Includes in
10 | # ------------------
11 |
12 | # include js, css files in header of desk.html
13 | # app_include_css = "/assets/mailrun/css/mailrun.css"
14 | # app_include_js = "/assets/mailrun/js/mailrun.js"
15 |
16 | # include js, css files in header of web template
17 | # web_include_css = "/assets/mailrun/css/mailrun.css"
18 | # web_include_js = "/assets/mailrun/js/mailrun.js"
19 |
20 | # include custom scss in every website theme (without file extension ".scss")
21 | # website_theme_scss = "mailrun/public/scss/website"
22 |
23 | # include js, css files in header of web form
24 | # webform_include_js = {"doctype": "public/js/doctype.js"}
25 | # webform_include_css = {"doctype": "public/css/doctype.css"}
26 |
27 | # include js in page
28 | # page_js = {"page" : "public/js/file.js"}
29 |
30 | # include js in doctype views
31 | # doctype_js = {"doctype" : "public/js/doctype.js"}
32 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
33 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
34 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
35 |
36 | # Svg Icons
37 | # ------------------
38 | # include app icons in desk
39 | # app_include_icons = "mailrun/public/icons.svg"
40 |
41 | # Home Pages
42 | # ----------
43 |
44 | # application home page (will override Website Settings)
45 | # home_page = "login"
46 |
47 | # website user home page (by Role)
48 | # role_home_page = {
49 | # "Role": "home_page"
50 | # }
51 |
52 | # Generators
53 | # ----------
54 |
55 | # automatically create page for each record of this doctype
56 | # website_generators = ["Web Page"]
57 |
58 | # Jinja
59 | # ----------
60 |
61 | # add methods and filters to jinja environment
62 | # jinja = {
63 | # "methods": "mailrun.utils.jinja_methods",
64 | # "filters": "mailrun.utils.jinja_filters"
65 | # }
66 |
67 | # Installation
68 | # ------------
69 |
70 | # before_install = "mailrun.install.before_install"
71 | # after_install = "mailrun.install.after_install"
72 |
73 | # Uninstallation
74 | # ------------
75 |
76 | # before_uninstall = "mailrun.uninstall.before_uninstall"
77 | # after_uninstall = "mailrun.uninstall.after_uninstall"
78 |
79 | # Integration Setup
80 | # ------------------
81 | # To set up dependencies/integrations with other apps
82 | # Name of the app being installed is passed as an argument
83 |
84 | # before_app_install = "mailrun.utils.before_app_install"
85 | # after_app_install = "mailrun.utils.after_app_install"
86 |
87 | # Integration Cleanup
88 | # -------------------
89 | # To clean up dependencies/integrations with other apps
90 | # Name of the app being uninstalled is passed as an argument
91 |
92 | # before_app_uninstall = "mailrun.utils.before_app_uninstall"
93 | # after_app_uninstall = "mailrun.utils.after_app_uninstall"
94 |
95 | # Desk Notifications
96 | # ------------------
97 | # See frappe.core.notifications.get_notification_config
98 |
99 | # notification_config = "mailrun.notifications.get_notification_config"
100 |
101 | # Permissions
102 | # -----------
103 | # Permissions evaluated in scripted ways
104 |
105 | # permission_query_conditions = {
106 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
107 | # }
108 | #
109 | # has_permission = {
110 | # "Event": "frappe.desk.doctype.event.event.has_permission",
111 | # }
112 |
113 | # DocType Class
114 | # ---------------
115 | # Override standard doctype classes
116 |
117 | # override_doctype_class = {
118 | # "ToDo": "custom_app.overrides.CustomToDo"
119 | # }
120 |
121 | # Document Events
122 | # ---------------
123 | # Hook on document methods and events
124 |
125 | # doc_events = {
126 | # "*": {
127 | # "on_update": "method",
128 | # "on_cancel": "method",
129 | # "on_trash": "method"
130 | # }
131 | # }
132 |
133 | # Scheduled Tasks
134 | # ---------------
135 |
136 | # scheduler_events = {
137 | # "all": [
138 | # "mailrun.tasks.all"
139 | # ],
140 | # "daily": [
141 | # "mailrun.tasks.daily"
142 | # ],
143 | # "hourly": [
144 | # "mailrun.tasks.hourly"
145 | # ],
146 | # "weekly": [
147 | # "mailrun.tasks.weekly"
148 | # ],
149 | # "monthly": [
150 | # "mailrun.tasks.monthly"
151 | # ],
152 | # }
153 |
154 | # Testing
155 | # -------
156 |
157 | # before_tests = "mailrun.install.before_tests"
158 |
159 | # Overriding Methods
160 | # ------------------------------
161 | #
162 | # override_whitelisted_methods = {
163 | # "frappe.desk.doctype.event.event.get_events": "mailrun.event.get_events"
164 | # }
165 | #
166 | # each overriding function accepts a `data` argument;
167 | # generated from the base implementation of the doctype dashboard,
168 | # along with any modifications made in other Frappe apps
169 | # override_doctype_dashboards = {
170 | # "Task": "mailrun.task.get_dashboard_data"
171 | # }
172 |
173 | # exempt linked doctypes from being automatically cancelled
174 | #
175 | # auto_cancel_exempted_doctypes = ["Auto Repeat"]
176 |
177 | # Ignore links to specified DocTypes when deleting documents
178 | # -----------------------------------------------------------
179 |
180 | # ignore_links_on_delete = ["Communication", "ToDo"]
181 |
182 | # Request Events
183 | # ----------------
184 | # before_request = ["mailrun.utils.before_request"]
185 | # after_request = ["mailrun.utils.after_request"]
186 |
187 | # Job Events
188 | # ----------
189 | # before_job = ["mailrun.utils.before_job"]
190 | # after_job = ["mailrun.utils.after_job"]
191 |
192 | # User Data Protection
193 | # --------------------
194 |
195 | # user_data_fields = [
196 | # {
197 | # "doctype": "{doctype_1}",
198 | # "filter_by": "{filter_by}",
199 | # "redact_fields": ["{field_1}", "{field_2}"],
200 | # "partial": 1,
201 | # },
202 | # {
203 | # "doctype": "{doctype_2}",
204 | # "filter_by": "{filter_by}",
205 | # "partial": 1,
206 | # },
207 | # {
208 | # "doctype": "{doctype_3}",
209 | # "strict": False,
210 | # },
211 | # {
212 | # "doctype": "{doctype_4}"
213 | # }
214 | # ]
215 |
216 | # Authentication and authorization
217 | # --------------------------------
218 |
219 | # auth_hooks = [
220 | # "mailrun.auth.validate"
221 | # ]
222 |
223 | website_route_rules = [{'from_route': '/mailrun/', 'to_route': 'mailrun'},]
--------------------------------------------------------------------------------
/mailrun/mailrun/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Commit-Company/mailrun/6a896b1ba3e0fa71aa70ef69fdba48ba8c03d7d4/mailrun/mailrun/__init__.py
--------------------------------------------------------------------------------
/mailrun/modules.txt:
--------------------------------------------------------------------------------
1 | MailRun
--------------------------------------------------------------------------------
/mailrun/patches.txt:
--------------------------------------------------------------------------------
1 | [pre_model_sync]
2 | # Patches added in this section will be executed before doctypes are migrated
3 | # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
4 |
5 | [post_model_sync]
6 | # Patches added in this section will be executed after doctypes are migrated
--------------------------------------------------------------------------------
/mailrun/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Commit-Company/mailrun/6a896b1ba3e0fa71aa70ef69fdba48ba8c03d7d4/mailrun/public/.gitkeep
--------------------------------------------------------------------------------
/mailrun/templates/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Commit-Company/mailrun/6a896b1ba3e0fa71aa70ef69fdba48ba8c03d7d4/mailrun/templates/__init__.py
--------------------------------------------------------------------------------
/mailrun/templates/pages/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Commit-Company/mailrun/6a896b1ba3e0fa71aa70ef69fdba48ba8c03d7d4/mailrun/templates/pages/__init__.py
--------------------------------------------------------------------------------
/mailrun/www/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Commit-Company/mailrun/6a896b1ba3e0fa71aa70ef69fdba48ba8c03d7d4/mailrun/www/__init__.py
--------------------------------------------------------------------------------
/mailrun/www/dashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Mailrun
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mailrun",
3 | "version": "1.0.0",
4 | "description": "Frappe app to manage newsletters",
5 | "main": "index.js",
6 | "scripts": {
7 | "postinstall": "cd dashboard && yarn install",
8 | "dev": "cd dashboard && yarn dev",
9 | "build": "cd dashboard && yarn build"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {}
15 | }
16 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "mailrun"
3 | authors = [
4 | { name = "Nikhil Kothari", email = "nik.kothari22@live.com"}
5 | ]
6 | description = "Frappe app to manage newsletters"
7 | requires-python = ">=3.10"
8 | readme = "README.md"
9 | dynamic = ["version"]
10 | dependencies = [
11 | # "frappe~=15.0.0" # Installed and managed by bench.
12 | ]
13 |
14 | [build-system]
15 | requires = ["flit_core >=3.4,<4"]
16 | build-backend = "flit_core.buildapi"
17 |
18 | # These dependencies are only installed when developer mode is enabled
19 | [tool.bench.dev-dependencies]
20 | # package_name = "~=1.1.0"
21 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
|