From 6a8a4a13ba3b9e0b63a2c22ef8a6e9542b0a6f9c Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Sun, 16 Feb 2025 21:25:45 -0700 Subject: [PATCH] create webapp stencil with side bar, routes and fake login --- cmd/cli/templates/vue/AboutView.vue.tmpl | 14 +- cmd/cli/templates/vue/App.vue.tmpl | 11 + cmd/cli/templates/vue/Header.vue.tmpl | 5 + .../vue/IconArrowFromShapeRight.vue.tmpl | 6 + cmd/cli/templates/vue/IconGear.vue.tmpl | 6 + cmd/cli/templates/vue/IconHome.vue.tmpl | 13 ++ cmd/cli/templates/vue/LoginLayout.vue.tmpl | 11 + cmd/cli/templates/vue/MainLayout.vue.tmpl | 67 ++++++ cmd/cli/templates/vue/Password.vue.tmpl | 21 ++ cmd/cli/templates/vue/SideNavBar.vue.tmpl | 193 ++++++++++++++++++ cmd/cli/templates/vue/authentication.ts.tmpl | 21 ++ cmd/cli/templates/vue/router-index.ts.tmpl | 76 +++++++ cmd/cli/webapp.go | 98 +++++++++ 13 files changed, 533 insertions(+), 9 deletions(-) create mode 100644 cmd/cli/templates/vue/App.vue.tmpl create mode 100644 cmd/cli/templates/vue/Header.vue.tmpl create mode 100644 cmd/cli/templates/vue/IconArrowFromShapeRight.vue.tmpl create mode 100644 cmd/cli/templates/vue/IconGear.vue.tmpl create mode 100644 cmd/cli/templates/vue/IconHome.vue.tmpl create mode 100644 cmd/cli/templates/vue/LoginLayout.vue.tmpl create mode 100644 cmd/cli/templates/vue/MainLayout.vue.tmpl create mode 100644 cmd/cli/templates/vue/Password.vue.tmpl create mode 100644 cmd/cli/templates/vue/SideNavBar.vue.tmpl create mode 100644 cmd/cli/templates/vue/authentication.ts.tmpl create mode 100644 cmd/cli/templates/vue/router-index.ts.tmpl diff --git a/cmd/cli/templates/vue/AboutView.vue.tmpl b/cmd/cli/templates/vue/AboutView.vue.tmpl index b466f70..272b243 100644 --- a/cmd/cli/templates/vue/AboutView.vue.tmpl +++ b/cmd/cli/templates/vue/AboutView.vue.tmpl @@ -1,15 +1,11 @@ diff --git a/cmd/cli/templates/vue/App.vue.tmpl b/cmd/cli/templates/vue/App.vue.tmpl new file mode 100644 index 0000000..d2c1397 --- /dev/null +++ b/cmd/cli/templates/vue/App.vue.tmpl @@ -0,0 +1,11 @@ + + + + + diff --git a/cmd/cli/templates/vue/Header.vue.tmpl b/cmd/cli/templates/vue/Header.vue.tmpl new file mode 100644 index 0000000..b7858c1 --- /dev/null +++ b/cmd/cli/templates/vue/Header.vue.tmpl @@ -0,0 +1,5 @@ + + diff --git a/cmd/cli/templates/vue/IconArrowFromShapeRight.vue.tmpl b/cmd/cli/templates/vue/IconArrowFromShapeRight.vue.tmpl new file mode 100644 index 0000000..94790db --- /dev/null +++ b/cmd/cli/templates/vue/IconArrowFromShapeRight.vue.tmpl @@ -0,0 +1,6 @@ + diff --git a/cmd/cli/templates/vue/IconGear.vue.tmpl b/cmd/cli/templates/vue/IconGear.vue.tmpl new file mode 100644 index 0000000..bd5c5bf --- /dev/null +++ b/cmd/cli/templates/vue/IconGear.vue.tmpl @@ -0,0 +1,6 @@ + + diff --git a/cmd/cli/templates/vue/IconHome.vue.tmpl b/cmd/cli/templates/vue/IconHome.vue.tmpl new file mode 100644 index 0000000..5d68a82 --- /dev/null +++ b/cmd/cli/templates/vue/IconHome.vue.tmpl @@ -0,0 +1,13 @@ + diff --git a/cmd/cli/templates/vue/LoginLayout.vue.tmpl b/cmd/cli/templates/vue/LoginLayout.vue.tmpl new file mode 100644 index 0000000..5cb2070 --- /dev/null +++ b/cmd/cli/templates/vue/LoginLayout.vue.tmpl @@ -0,0 +1,11 @@ + + + diff --git a/cmd/cli/templates/vue/MainLayout.vue.tmpl b/cmd/cli/templates/vue/MainLayout.vue.tmpl new file mode 100644 index 0000000..330d773 --- /dev/null +++ b/cmd/cli/templates/vue/MainLayout.vue.tmpl @@ -0,0 +1,67 @@ + + + diff --git a/cmd/cli/templates/vue/Password.vue.tmpl b/cmd/cli/templates/vue/Password.vue.tmpl new file mode 100644 index 0000000..bf15818 --- /dev/null +++ b/cmd/cli/templates/vue/Password.vue.tmpl @@ -0,0 +1,21 @@ + + + diff --git a/cmd/cli/templates/vue/SideNavBar.vue.tmpl b/cmd/cli/templates/vue/SideNavBar.vue.tmpl new file mode 100644 index 0000000..faa2b6b --- /dev/null +++ b/cmd/cli/templates/vue/SideNavBar.vue.tmpl @@ -0,0 +1,193 @@ + + + + + diff --git a/cmd/cli/templates/vue/authentication.ts.tmpl b/cmd/cli/templates/vue/authentication.ts.tmpl new file mode 100644 index 0000000..677e512 --- /dev/null +++ b/cmd/cli/templates/vue/authentication.ts.tmpl @@ -0,0 +1,21 @@ +import { ref } from 'vue'; +import { defineStore } from 'pinia'; + +export const useAuthenticationStore = defineStore('authentication', () => { + // TODO: this should be replaced with a request to the server to check if the user is authenticated + const isAuthenticated = ref(localStorage.getItem('isAuthenticated') === 'true'); + + function login() { + // TODO: make a request to the server to authenticate the user + isAuthenticated.value = true; + localStorage.setItem('isAuthenticated', 'true'); + } + + function logout() { + // TODO: make a request to the server to log out the user + isAuthenticated.value = false; + localStorage.setItem('isAuthenticated', 'false'); + } + + return { isAuthenticated, login, logout }; +}); diff --git a/cmd/cli/templates/vue/router-index.ts.tmpl b/cmd/cli/templates/vue/router-index.ts.tmpl new file mode 100644 index 0000000..6ecd206 --- /dev/null +++ b/cmd/cli/templates/vue/router-index.ts.tmpl @@ -0,0 +1,76 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthenticationStore } from '@/stores/authentication.ts' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/login', + name: 'Login', + component: () => import('../layouts/Login.vue'), // Lazy-loaded + meta: { public: true }, // Indicates this route doesn’t require auth + children: [ + { + path: '', + name: 'Password', + component: () => import('../views/Password.vue'), + }, + ], + }, + { + path: '/', + component: () => import('../layouts/MainLayout.vue'), // Protected layout + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'Home', + component: () => import('../views/HomeView.vue'), + }, + { + path: '/overview', + name: 'Overview', + component: () => import('../views/AboutView.vue'), + }, + { + path: '/settings', + name: 'Settings', + component: () => import('../views/HomeView.vue'), + }, + ], + }, + { + path: '/logout', + name: 'Logout', + meta: { public: true }, + redirect: '/login', + }, + ], +}); + +router.beforeEach(async (to, from, next) => { + const authenticationStore = useAuthenticationStore(); + + if (to.redirectedFrom?.path === '/logout') { + authenticationStore.logout(); + } + + const isAuthenticated = await Promise.resolve(authenticationStore.isAuthenticated); /* logic to check auth state, e.g., from a store */ + if (to.meta.requiresAuth && !isAuthenticated) { + // save the desired route in localStorage and redirect to login + localStorage.setItem('redirect', to.fullPath); + return next({ path: '/login' }); + } + // if (to.meta.public && isAuthenticated) { + // // TODO: Decide if you want to redirect to a different page if the user is already authenticated + // } + const deepLink = localStorage.getItem('redirect'); + if (to.meta.requiresAuth && isAuthenticated && !!deepLink) { + // remove redirect from localStorage and redirect to the deep link + localStorage.removeItem('redirect'); + return next({ path: deepLink }); + } + next(); +}); + +export default router diff --git a/cmd/cli/webapp.go b/cmd/cli/webapp.go index 981b5c8..5560485 100644 --- a/cmd/cli/webapp.go +++ b/cmd/cli/webapp.go @@ -3,9 +3,12 @@ package main import ( _ "embed" "fmt" + "golang.org/x/text/cases" + "golang.org/x/text/language" "os" "os/exec" "strings" + "text/template" ) //go:embed templates/vue/AboutView.vue.tmpl @@ -14,6 +17,39 @@ var aboutViewTemplate string //go:embed templates/vue/HomeView.vue.tmpl var homeViewTemplate string +//go:embed templates/vue/SideNavBar.vue.tmpl +var sideNavBarTemplate string + +//go:embed templates/vue/Header.vue.tmpl +var headerTemplateSource string + +//go:embed templates/vue/LoginLayout.vue.tmpl +var loginLayoutTemplate string + +//go:embed templates/vue/MainLayout.vue.tmpl +var mainLayoutTemplate string + +//go:embed templates/vue/Password.vue.tmpl +var passwordTemplate string + +//go:embed templates/vue/authentication.ts.tmpl +var authenticationTemplate string + +//go:embed templates/vue/App.vue.tmpl +var vueAppTemplate string + +//go:embed templates/vue/router-index.ts.tmpl +var routerIndexTemplate string + +//go:embed templates/vue/IconArrowFromShapeRight.vue.tmpl +var iconArrowFromShapeRightTemplate string + +//go:embed templates/vue/IconHome.vue.tmpl +var iconHomeTemplate string + +//go:embed templates/vue/IconGear.vue.tmpl +var iconGearTemplate string + func setupWebapp(name string) error { fmt.Println("Setting up webapp with Vue 3, TypeScript, Vue Router, Pinia, and ESLint with Prettier") cmd := exec.Command("npm", "create", "vue@latest", "--", "--ts", "--router", "--pinia", "--eslint-with-prettier", name) @@ -67,6 +103,12 @@ func setupWebapp(name string) error { "src/assets/logo.svg", }) + // make sure "src/layouts/" exists + err = os.Mkdir("src/layouts", 0755) + if err != nil { + return fmt.Errorf("error creating layouts directory | %w", err) + } + // create a new about view from the aboutViewTemplate err = os.WriteFile("src/views/AboutView.vue", []byte(aboutViewTemplate), 0644) if err != nil { @@ -78,6 +120,48 @@ func setupWebapp(name string) error { return fmt.Errorf("error writing new HomeView.vue | %w", err) } + // create src/components/SideNavBar.vue from a template + err = os.WriteFile("src/components/SideNavBar.vue", []byte(sideNavBarTemplate), 0644) + if err != nil { + return fmt.Errorf("error writing new SideNavBar.vue | %w", err) + } + + titleMaker := cases.Title(language.English) + + // create src/components/Header.vue from a template + err = renderTemplate("src/components/Header.vue", headerTemplateSource, map[string]string{"AppName": strings.ToLower(name), "AppNameCaps": titleMaker.String(name)}) + if err != nil { + return fmt.Errorf("error writing new Header.vue | %w", err) + } + + err = renderTemplate("src/layouts/MainLayout.vue", mainLayoutTemplate, map[string]string{"AppName": strings.ToLower(name), "AppNameCaps": titleMaker.String(name)}) + if err != nil { + return fmt.Errorf("error writing new MainLayout.vue | %w", err) + } + + // create src/layouts/Login.vue from a template + err = os.WriteFile("src/layouts/Login.vue", []byte(loginLayoutTemplate), 0644) + if err != nil { + return fmt.Errorf("error writing new Login.vue | %w", err) + } + + straightCopies := []struct{ src, dest string }{ + {vueAppTemplate, "src/App.vue"}, + {passwordTemplate, "src/views/Password.vue"}, + {authenticationTemplate, "src/stores/authentication.ts"}, + {routerIndexTemplate, "src/router/index.ts"}, + {iconArrowFromShapeRightTemplate, "src/components/icons/IconArrowFromShapeRight.vue"}, + {iconHomeTemplate, "src/components/icons/IconHome.vue"}, + {iconGearTemplate, "src/components/icons/IconGear.vue"}, + } + + for _, srcDest := range straightCopies { + err = os.WriteFile(srcDest.dest, []byte(srcDest.src), 0644) + if err != nil { + return fmt.Errorf("error writing new file %s | %w", srcDest.dest, err) + } + } + fmt.Println("Exiting the new directory") err = os.Chdir("..") if err != nil { @@ -87,6 +171,20 @@ func setupWebapp(name string) error { return nil } +func renderTemplate(fileLocation string, templateSource string, templateData map[string]string) error { + headerFile, err := os.Create(fileLocation) + if err != nil { + return fmt.Errorf("error creating new file %s | %w", fileLocation, err) + } + defer headerFile.Close() + headerTemplate := template.Must(template.New("header").Parse(templateSource)) + err = headerTemplate.Execute(headerFile, templateData) + if err != nil { + return fmt.Errorf("error rendering file %s | %w", fileLocation, err) + } + return nil +} + func setupTailwind() error { // check that there is a vite.config.ts file and a package.json file _, err := os.Stat("vite.config.ts")