feat: billing page in frappe

This commit is contained in:
Shariq Ansari 2024-11-11 18:44:40 +05:30
parent fd0e4f04eb
commit 92920bc232
54 changed files with 377 additions and 13 deletions

6
.gitignore vendored
View file

@ -198,3 +198,9 @@ cypress/videos
.helix/
# Aider AI Chat
.aider*
# frappecloud billing
billing/node_modules
frappe/public/billing
billing/yarn.lock
frappe/www/billing.html

5
billing/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

4
billing/.prettierrc.json Normal file
View file

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

42
billing/README.md Normal file
View file

@ -0,0 +1,42 @@
# Frappe UI Starter
This template should help get you started developing custom frontend for Frappe
apps with Vue 3 and the Frappe UI package.
This boilerplate sets up Vue 3, Vue Router, TailwindCSS, and Frappe UI out of
the box.
## Usage
This template is meant to be cloned inside an existing Frappe App. Assuming your
apps name is `todo`. Clone this template in the root folder of your app using `degit`.
```
cd apps/todo
npx degit netchampfaris/frappe-ui-starter frontend
cd frontend
yarn
yarn dev
```
In a development environment, you need to put the below key-value pair in your `site_config.json` file:
```
"ignore_csrf": 1
```
This will prevent `CSRFToken` errors while using the vite dev server. In production environment, the `csrf_token` is attached to the `window` object in `index.html` for you.
The Vite dev server will start on the port `8080`. This can be changed from `vite.config.js`.
The development server is configured to proxy your frappe app (usually running on port `8000`). If you have a site named `todo.test`, open `http://todo.test:8080` in your browser. If you see a button named "Click to send 'ping' request", congratulations!
If you notice the browser URL is `/frontend`, this is the base URL where your frontend app will run in production.
To change this, open `src/router.js` and change the base URL passed to `createWebHistory`.
## Resources
- [Vue 3](https://v3.vuejs.org/guide/introduction.html)
- [Vue Router](https://next.router.vuejs.org/guide/)
- [Frappe UI](https://github.com/frappe/frappe-ui)
- [TailwindCSS](https://tailwindcss.com/docs/utility-first)
- [Vite](https://vitejs.dev/guide/)

18
billing/index.html Normal file
View file

@ -0,0 +1,18 @@
<!doctype html>
<html class="h-full" lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover maximum-scale=1.0, user-scalable=no"
/>
<title>Billing</title>
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Billing" />
<meta name="apple-mobile-web-app-status-bar-style" content="white" />
</head>
<body class="sm:overscroll-y-none no-scrollbar">
<div id="app" class="h-full"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

22
billing/package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "billing-ui",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build --base=/assets/frappe/billing/ && yarn copy-html-entry",
"copy-html-entry": "cp ../frappe/public/billing/index.html ../frappe/www/billing.html",
"serve": "vite preview"
},
"dependencies": {
"frappe-ui": "^v0.1.72",
"tailwindcss": "^3.3.3",
"vue": "^3.4.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.5",
"vite": "^4"
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
billing/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

7
billing/src/App.vue Normal file
View file

@ -0,0 +1,7 @@
<template>
<div><Button label="Click Me" /></div>
</template>
<script setup>
import { Button } from 'frappe-ui'
</script>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,152 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url("Inter-Thin.woff2?v=3.12") format("woff2"),
url("Inter-Thin.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"),
url("Inter-ThinItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLight.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLightItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("Inter-Light.woff2?v=3.12") format("woff2"),
url("Inter-Light.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"),
url("Inter-LightItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("Inter-Regular.woff2?v=3.12") format("woff2"),
url("Inter-Regular.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("Inter-Italic.woff2?v=3.12") format("woff2"),
url("Inter-Italic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("Inter-Medium.woff2?v=3.12") format("woff2"),
url("Inter-Medium.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"),
url("Inter-MediumItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"),
url("Inter-SemiBold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-SemiBoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("Inter-Bold.woff2?v=3.12") format("woff2"),
url("Inter-Bold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-BoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("Inter-Black.woff2?v=3.12") format("woff2"),
url("Inter-Black.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"),
url("Inter-BlackItalic.woff?v=3.12") format("woff");
}

26
billing/src/index.css Normal file
View file

@ -0,0 +1,26 @@
@import './assets/Inter/inter.css';
@import 'frappe-ui/src/style.css';
@layer components {
.prose-f {
@apply
break-all
max-w-none
prose
prose-code:break-all
prose-code:whitespace-pre-wrap
prose-img:border
prose-img:rounded-lg
prose-sm
prose-table:table-fixed
prose-td:border
prose-td:border-gray-300
prose-td:p-2
prose-td:relative
prose-th:bg-gray-100
prose-th:border
prose-th:border-gray-300
prose-th:p-2
prose-th:relative
}
}

8
billing/src/main.js Normal file
View file

@ -0,0 +1,8 @@
import './index.css'
import { createApp } from 'vue'
import App from './App.vue'
let app = createApp(App)
app.mount('#app')

View file

@ -0,0 +1,17 @@
module.exports = {
presets: [require('frappe-ui/src/utils/tailwind.config')],
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
'../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
],
safelist: [
{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] },
{ pattern: /^grid-cols-/ },
],
theme: {
extend: {},
},
plugins: [],
}

48
billing/vite.config.js Normal file
View file

@ -0,0 +1,48 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import frappeui from 'frappe-ui/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
frappeui(),
vue(),
{
name: 'transform-index.html',
transformIndexHtml(html, context) {
if (!context.server) {
return html.replace(
/<\/body>/,
`
<script>
{% for key in boot %}
window["{{ key }}"] = {{ boot[key] | tojson }};
{% endfor %}
</script>
</body>
`,
)
}
return html
},
},
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
outDir: '../frappe/public/billing',
emptyOutDir: true,
commonjsOptions: {
include: [/tailwind.config.js/, /node_modules/],
},
// minify: false,
sourcemap: true,
},
optimizeDeps: {
include: ['tailwind.config.js'],
},
})

View file

@ -71,12 +71,12 @@ const argv = yargs
.example("node esbuild --apps frappe,erpnext", "Run build only for frappe and erpnext")
.example(
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
"Run build only for specified bundles"
"Run build only for specified bundles",
)
.version(false).argv;
const APPS = (!argv.apps ? app_list : argv.apps.split(",")).filter(
(app) => !(argv.skip_frappe && app == "frappe")
(app) => !(argv.skip_frappe && app == "frappe"),
);
const FILES_TO_BUILD = argv.files ? argv.files.split(",") : [];
const WATCH_MODE = Boolean(argv.watch);
@ -88,7 +88,7 @@ const NODE_PATHS = [].concat(
// node_modules of apps directly importable
app_list.map((app) => path.resolve(apps_path, app, "node_modules")).filter(fs.existsSync),
// import js file of any app if you provide the full path
app_list.map((app) => path.resolve(apps_path, app)).filter(fs.existsSync)
app_list.map((app) => path.resolve(apps_path, app)).filter(fs.existsSync),
);
const USING_CACHED = Boolean(argv["using-cached"]);
@ -250,11 +250,11 @@ function get_all_files_to_build(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
include_patterns.push(
path.resolve(public_path, "**", "*.bundle.{js,ts,css,sass,scss,less,styl,jsx}")
path.resolve(public_path, "**", "*.bundle.{js,ts,css,sass,scss,less,styl,jsx}"),
);
ignore_patterns.push(
path.resolve(public_path, "node_modules"),
path.resolve(public_path, "dist")
path.resolve(public_path, "dist"),
);
}
@ -275,7 +275,7 @@ function get_files_to_build(files) {
include_patterns.push(path.resolve(public_path, "**", bundle));
ignore_patterns.push(
path.resolve(public_path, "node_modules"),
path.resolve(public_path, "dist")
path.resolve(public_path, "dist"),
);
}
@ -341,7 +341,7 @@ function get_watch_config() {
notify_redis({ error });
} else {
let { new_assets_json, prev_assets_json } = await write_assets_json(
result.metafile
result.metafile,
);
let changed_files;
@ -379,7 +379,7 @@ function log_built_assets(results) {
{
text: chalk.cyan.bold("Size"),
width: column_widths[1],
}
},
);
cliui.div("");
@ -422,7 +422,7 @@ function log_built_assets(results) {
{
text: file.size,
width: column_widths[1],
}
},
);
}
cliui.div("");
@ -503,9 +503,12 @@ function run_build_command_for_apps(apps) {
let { execSync } = require("child_process");
for (let app of apps) {
if (app === "frappe") continue;
// if (app === "frappe") continue;
let root_app_path = path.resolve(apps_path, app);
let root_app_path =
app === "frappe"
? path.resolve(apps_path, app, "billing")
: path.resolve(apps_path, app);
let package_json = path.resolve(root_app_path, "package.json");
let node_modules = path.resolve(root_app_path, "node_modules");
@ -521,7 +524,7 @@ function run_build_command_for_apps(apps) {
process.chdir(root_app_path);
if (!fs.existsSync(node_modules)) {
log(
`\nInstalling dependencies for ${chalk.bold(app)} (because node_modules not found)`
`\nInstalling dependencies for ${chalk.bold(app)} (because node_modules not found)`,
);
execSync("yarn install", { encoding: "utf8", stdio: "inherit" });
}
@ -568,7 +571,7 @@ async function notify_redis({ error, success, changed_files }) {
JSON.stringify({
event: "build_event",
message: payload,
})
}),
);
}