diff --git a/src/constants/templates.ts b/src/constants/templates.ts
index 2ea23ba..36c6837 100644
--- a/src/constants/templates.ts
+++ b/src/constants/templates.ts
@@ -81,5 +81,12 @@ export const TEMPLATES: Template[] = [
type: 'Example',
description: APP_DESCRIPTION_EXAMPLE,
kind: 'app'
+ },
+ {
+ framework: `Vanilla JavaScript`,
+ key: `vanilla-js-example`,
+ type: 'Example',
+ description: APP_DESCRIPTION_EXAMPLE,
+ kind: 'app'
}
];
diff --git a/src/services/generate.services.ts b/src/services/generate.services.ts
index a827aee..d5120f7 100644
--- a/src/services/generate.services.ts
+++ b/src/services/generate.services.ts
@@ -159,7 +159,7 @@ const removeLocalConfig = async ({where, template}: PopulateInputFn) => {
? 'astro.config.mjs'
: template.framework === 'Next.js'
? 'next.config.mjs'
- : template.framework === 'React'
+ : ['React', 'Vanilla JavaScript'].includes(template.framework)
? 'vite.config.js'
: 'vite.config.ts'
);
diff --git a/src/types/template.ts b/src/types/template.ts
index ad74bf4..84deced 100644
--- a/src/types/template.ts
+++ b/src/types/template.ts
@@ -8,6 +8,13 @@ export interface Template {
kind: ProjectKind;
}
-export type TemplateFramework = 'Angular' | 'Astro' | 'Next.js' | 'React' | 'SvelteKit' | 'Vue';
+export type TemplateFramework =
+ | 'Angular'
+ | 'Astro'
+ | 'Next.js'
+ | 'React'
+ | 'SvelteKit'
+ | 'Vue'
+ | 'Vanilla JavaScript';
export type TemplateType = 'Starter' | 'Example';
diff --git a/templates/vanilla-js-example/.gitignore b/templates/vanilla-js-example/.gitignore
new file mode 100644
index 0000000..91edcc2
--- /dev/null
+++ b/templates/vanilla-js-example/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+public/workers
\ No newline at end of file
diff --git a/templates/vanilla-js-example/docker-compose.yml b/templates/vanilla-js-example/docker-compose.yml
new file mode 100644
index 0000000..58ef77e
--- /dev/null
+++ b/templates/vanilla-js-example/docker-compose.yml
@@ -0,0 +1,12 @@
+services:
+ juno-satellite:
+ image: junobuild/satellite:latest
+ ports:
+ - 5987:5987
+ volumes:
+ - juno_satellite:/juno/.juno
+ - ./juno.dev.config.js:/juno/juno.dev.config.js
+ - ./target/deploy:/juno/target/deploy/
+
+volumes:
+ juno_satellite:
diff --git a/templates/vanilla-js-example/index.html b/templates/vanilla-js-example/index.html
new file mode 100644
index 0000000..2636f39
--- /dev/null
+++ b/templates/vanilla-js-example/index.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+ Juno / Vanilla JavaScript Example
+
+
+
+
+
+
+
+
+
+ Example App
+
+
+ Explore this demo app built with vanilla JavaScript, Tailwind, and
+
+ Juno, showcasing a practical application of these technologies.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/vanilla-js-example/juno.dev.config.js b/templates/vanilla-js-example/juno.dev.config.js
new file mode 100644
index 0000000..dca60ab
--- /dev/null
+++ b/templates/vanilla-js-example/juno.dev.config.js
@@ -0,0 +1,27 @@
+import {defineDevConfig} from '@junobuild/config';
+
+/** @type {import('@junobuild/config').JunoDevConfig} */
+export default defineDevConfig(() => ({
+ satellite: {
+ collections: {
+ db: [
+ {
+ collection: 'notes',
+ read: 'managed',
+ write: 'managed',
+ memory: 'stable',
+ mutablePermissions: true
+ }
+ ],
+ storage: [
+ {
+ collection: 'images',
+ read: 'managed',
+ write: 'managed',
+ memory: 'stable',
+ mutablePermissions: true
+ }
+ ]
+ }
+ }
+}));
diff --git a/templates/vanilla-js-example/package.json b/templates/vanilla-js-example/package.json
new file mode 100644
index 0000000..657f88a
--- /dev/null
+++ b/templates/vanilla-js-example/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "vanilla-js-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "format": "prettier --write .",
+ "postinstall:copy-auth": "node -e \"require('fs').cpSync('node_modules/@junobuild/core/dist/workers/', './public/workers', {recursive: true});\"",
+ "postinstall": "npm run postinstall:copy-auth"
+ },
+ "devDependencies": {
+ "@junobuild/config": "^0.0.6",
+ "@junobuild/vite-plugin": "^0.0.12",
+ "autoprefixer": "^10.4.19",
+ "prettier": "^3.2.5",
+ "tailwindcss": "^3.4.3",
+ "vite": "^5.2.0"
+ },
+ "dependencies": {
+ "@junobuild/core": "^0.0.49",
+ "nanoid": "^5.0.7"
+ }
+}
diff --git a/templates/vanilla-js-example/postcss.config.js b/templates/vanilla-js-example/postcss.config.js
new file mode 100644
index 0000000..ba80730
--- /dev/null
+++ b/templates/vanilla-js-example/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {}
+ }
+};
diff --git a/templates/vanilla-js-example/public/fonts/jetbrainsmono-bold.woff2 b/templates/vanilla-js-example/public/fonts/jetbrainsmono-bold.woff2
new file mode 100644
index 0000000..4917f43
Binary files /dev/null and b/templates/vanilla-js-example/public/fonts/jetbrainsmono-bold.woff2 differ
diff --git a/templates/vanilla-js-example/public/fonts/jetbrainsmono-regular.woff2 b/templates/vanilla-js-example/public/fonts/jetbrainsmono-regular.woff2
new file mode 100644
index 0000000..40da427
Binary files /dev/null and b/templates/vanilla-js-example/public/fonts/jetbrainsmono-regular.woff2 differ
diff --git a/templates/vanilla-js-example/public/juno_illustration.svg b/templates/vanilla-js-example/public/juno_illustration.svg
new file mode 100644
index 0000000..c0a4d95
--- /dev/null
+++ b/templates/vanilla-js-example/public/juno_illustration.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/templates/vanilla-js-example/public/vite.svg b/templates/vanilla-js-example/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/templates/vanilla-js-example/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/templates/vanilla-js-example/src/components/content.js b/templates/vanilla-js-example/src/components/content.js
new file mode 100644
index 0000000..2d99832
--- /dev/null
+++ b/templates/vanilla-js-example/src/components/content.js
@@ -0,0 +1,14 @@
+import {renderLogout} from './logout';
+import {renderModal} from './modal';
+import {renderTable} from './table';
+
+export const renderContent = (app) => {
+ app.innerHTML = `
+
+ ${renderTable(app)}
+
+ ${renderModal(app)}
+
+ ${renderLogout(app)}
+
`;
+};
diff --git a/templates/vanilla-js-example/src/components/delete.js b/templates/vanilla-js-example/src/components/delete.js
new file mode 100644
index 0000000..696054e
--- /dev/null
+++ b/templates/vanilla-js-example/src/components/delete.js
@@ -0,0 +1,60 @@
+import {deleteAsset, deleteDoc} from '@junobuild/core';
+import {addEventClick, reload} from '../utils/utils';
+
+const deleteItem = async (item) => {
+ try {
+ const {
+ data: {url}
+ } = item;
+
+ if (url !== undefined) {
+ const {pathname: fullPath} = new URL(url);
+
+ await deleteAsset({
+ collection: 'images',
+ fullPath
+ });
+ }
+
+ await deleteDoc({
+ collection: 'notes',
+ doc: item
+ });
+
+ reload();
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+export const renderDelete = ({table, item, index}) => {
+ addEventClick({
+ target: table,
+ selector: `#deleteItem${index}`,
+ fn: async () => await deleteItem(item)
+ });
+
+ return ``;
+};
diff --git a/templates/vanilla-js-example/src/components/login.js b/templates/vanilla-js-example/src/components/login.js
new file mode 100644
index 0000000..65b8393
--- /dev/null
+++ b/templates/vanilla-js-example/src/components/login.js
@@ -0,0 +1,16 @@
+import {signIn} from '@junobuild/core';
+import {addEventClick} from '../utils/utils';
+
+export const renderLogin = (app) => {
+ addEventClick({
+ target: app,
+ selector: '#login',
+ fn: signIn
+ });
+
+ app.innerHTML = `
+`;
+};
diff --git a/templates/vanilla-js-example/src/components/logout.js b/templates/vanilla-js-example/src/components/logout.js
new file mode 100644
index 0000000..e950326
--- /dev/null
+++ b/templates/vanilla-js-example/src/components/logout.js
@@ -0,0 +1,27 @@
+import {signOut} from '@junobuild/core';
+import {addEventClick} from '../utils/utils';
+
+export const renderLogout = (app) => {
+ addEventClick({
+ target: app,
+ selector: '#logout',
+ fn: signOut
+ });
+
+ return ``;
+};
diff --git a/templates/vanilla-js-example/src/components/modal.js b/templates/vanilla-js-example/src/components/modal.js
new file mode 100644
index 0000000..9a1a956
--- /dev/null
+++ b/templates/vanilla-js-example/src/components/modal.js
@@ -0,0 +1,145 @@
+import {authSubscribe, setDoc, uploadFile} from '@junobuild/core';
+import {nanoid} from 'nanoid';
+import {addEventClick, reload} from '../utils/utils';
+
+let user;
+authSubscribe((u) => (user = u));
+
+const submitEntry = async (modal) => {
+ // Demo purpose therefore edge case not properly handled
+ if ([null, undefined].includes(user)) {
+ return;
+ }
+
+ const textarea = modal.querySelector('#entryText');
+ const input = modal.querySelector('#fileEntry');
+
+ const inputText = textarea.value;
+ const file = input.files[0];
+
+ try {
+ let url;
+
+ if (file !== undefined) {
+ const filename = `${user.key}-${file.name}`;
+
+ const {downloadUrl} = await uploadFile({
+ collection: 'images',
+ data: file,
+ filename
+ });
+
+ url = downloadUrl;
+ }
+
+ const key = nanoid();
+
+ await setDoc({
+ collection: 'notes',
+ doc: {
+ key,
+ data: {
+ text: inputText,
+ ...(url !== undefined && {url})
+ }
+ }
+ });
+
+ closeModal(modal);
+
+ reload();
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+const closeModal = (modal) => (modal.innerHTML = '');
+
+const showModal = () => {
+ const modal = document.querySelector('#modal');
+
+ addEventClick({
+ target: modal,
+ selector: '#closeModal',
+ fn: () => closeModal(modal)
+ });
+
+ addEventClick({
+ target: modal,
+ selector: '#submitEntry',
+ fn: async () => await submitEntry(modal)
+ });
+
+ modal.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+};
+
+export const renderModal = (app) => {
+ addEventClick({
+ target: app,
+ selector: '#addEntry',
+ fn: showModal
+ });
+
+ return `
+
+
+`;
+};
diff --git a/templates/vanilla-js-example/src/components/table.js b/templates/vanilla-js-example/src/components/table.js
new file mode 100644
index 0000000..16172eb
--- /dev/null
+++ b/templates/vanilla-js-example/src/components/table.js
@@ -0,0 +1,80 @@
+import {listDocs} from '@junobuild/core';
+import {renderDelete} from './delete';
+
+const list = async () => {
+ const {items} = await listDocs({
+ collection: 'notes',
+ filter: {}
+ });
+
+ const table = document.querySelector('#table');
+
+ table.innerHTML = `
+
+
+ Entries
+
+
+
+
+ ${items
+ .map((item, index) => {
+ const {
+ key,
+ data: {text, url}
+ } = item;
+
+ return `
+
+ ${index + 1} )
+
+
+ ${text}
+
+
+ ${
+ url !== undefined
+ ? `
+
+
+ `
+ : ''
+ }
+
+ ${renderDelete({table, item, index})}
+
+
`;
+ })
+ .join('')}
+
+
`;
+};
+
+export const renderTable = (app) => {
+ const observer = new MutationObserver(async () => {
+ observer.disconnect();
+
+ await list();
+ });
+ observer.observe(app, {childList: true, subtree: true});
+
+ return ``;
+};
+
+window.addEventListener('reload', list);
diff --git a/templates/vanilla-js-example/src/main.js b/templates/vanilla-js-example/src/main.js
new file mode 100644
index 0000000..80cfa73
--- /dev/null
+++ b/templates/vanilla-js-example/src/main.js
@@ -0,0 +1,34 @@
+import {authSubscribe, initJuno} from '@junobuild/core';
+import {renderContent} from './components/content';
+import {renderLogin} from './components/login';
+import './style.css';
+
+/**
+ * Global listener. When the user sign-in or sign-out, we re-render the app.
+ */
+authSubscribe((user) => {
+ const app = document.querySelector('#app');
+
+ if (user === null || user === undefined) {
+ renderLogin(app);
+ return;
+ }
+
+ renderContent(app);
+});
+
+/**
+ * When the app starts, we initialize Juno.
+ * @returns {Promise}
+ */
+const onAppInit = async () => {
+ await initJuno({
+ satelliteId: import.meta.env.VITE_SATELLITE_ID,
+ container: import.meta.env.VITE_CONTAINER,
+ workers: {
+ auth: true
+ }
+ });
+};
+
+document.addEventListener('DOMContentLoaded', onAppInit, {once: true});
diff --git a/templates/vanilla-js-example/src/style.css b/templates/vanilla-js-example/src/style.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/templates/vanilla-js-example/src/style.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/templates/vanilla-js-example/src/utils/utils.js b/templates/vanilla-js-example/src/utils/utils.js
new file mode 100644
index 0000000..fb1cbc8
--- /dev/null
+++ b/templates/vanilla-js-example/src/utils/utils.js
@@ -0,0 +1,18 @@
+/**
+ * Wait for an element to be rendered in the DOM before attaching a passive click event listener to it.
+ * @param target The element or parent which its content will be updated / re-rendered.
+ * @param selector The selector to the element to which the function should attach the click event.
+ * @param fn The function to trigger on click.
+ */
+export const addEventClick = ({target, selector, fn}) => {
+ const observer = new MutationObserver(() => {
+ observer.disconnect();
+ document.querySelector(selector)?.addEventListener('click', fn, {passive: true});
+ });
+ observer.observe(target, {childList: true, subtree: true});
+};
+
+export const reload = () => {
+ const event = new Event('reload');
+ window.dispatchEvent(event);
+};
diff --git a/templates/vanilla-js-example/tailwind.config.js b/templates/vanilla-js-example/tailwind.config.js
new file mode 100644
index 0000000..f7c5366
--- /dev/null
+++ b/templates/vanilla-js-example/tailwind.config.js
@@ -0,0 +1,45 @@
+import {fontFamily} from 'tailwindcss/defaultTheme';
+
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{html,js}'],
+ theme: {
+ fontFamily: {
+ sans: ['JetBrains Mono', 'sans-serif', ...fontFamily.sans]
+ },
+ extend: {
+ screens: {
+ tall: {raw: '(min-height: 800px)'}
+ },
+ animation: {
+ fade: 'fadeIn .25s ease-in-out'
+ },
+
+ keyframes: {
+ fadeIn: {
+ from: {opacity: '0'},
+ to: {opacity: '1'}
+ }
+ }
+ },
+ colors: {
+ inherit: 'inherit',
+ transparent: 'transparent',
+ current: 'currentColor',
+ black: 'rgb(0 0 0)',
+ white: 'rgb(255 255 255)',
+ ['lavender-blue']: {
+ 50: '#f2f3ff',
+ 100: '#e4e7ff',
+ 200: '#c9cfff',
+ 300: '#aeb8ff',
+ 400: '#93a0ff',
+ 500: '#7888ff',
+ 600: '#606dcc',
+ 700: '#485299',
+ 800: '#303666',
+ 900: '#181b33'
+ }
+ }
+ }
+};
diff --git a/templates/vanilla-js-example/vite.config.js b/templates/vanilla-js-example/vite.config.js
new file mode 100644
index 0000000..1ae7c16
--- /dev/null
+++ b/templates/vanilla-js-example/vite.config.js
@@ -0,0 +1,7 @@
+import juno from '@junobuild/vite-plugin';
+import {defineConfig} from 'vite';
+
+/** @type {import('vite').UserConfig} */
+export default defineConfig({
+ plugins: [juno({container: true})]
+});