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})] +});