Wednesday, May 7, 2025

TEMPLATE VUETIFY, VUE 2 DAN VUE-ROUTER versi 2

Berikut ini adalah skrip template vue dengan kecepatan dan keandalan yang luar biasa, karna pemanggilan dan penggunaan skrip sangatlah mudah.










<!DOCTYPE html>
<html lang="en" style="overflow: hidden;">

<head>
  <meta charset="utf-8">
  <title>Vue Vuetify Dialog Dynamic</title>
  <link href="https://cdn.jsdelivr.net/npm/@mdi/font/css/materialdesignicons.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/vuetify@2.5.10/dist/vuetify.min.css" rel="stylesheet">
</head>

<body>
  <div id="app">
    <v-app>
      <!-- Global Snackbar -->
      <v-snackbar v-model="snackbar.show" :timeout="snackbar.timeout" :color="snackbar.color">
        {{ snackbar.text }}
        <template v-slot:action="{ attrs }">
          <v-btn text v-bind="attrs" @click="snackbar.show = false">Tutup</v-btn>
        </template>
      </v-snackbar>

      <!-- Dialog Stack -->
      <template v-for="(dialog, index) in dialogs">
        <v-dialog
          :key="dialog.id"
          v-model="dialog.show"
          :max-width="dialog.width"
          :persistent="dialog.persistent"
          :z-index="500 + index * 10"
        >
          <v-card v-if="dialog.type !== 'loading'">
            <v-toolbar dark class="mb-4" color="primary">
              <v-toolbar-title>{{ dialog.title }}</v-toolbar-title>
            </v-toolbar>
            <v-card-text>
              <div v-if="dialog.type === 'info'">{{ dialog.content }}</div>
              <component v-else :is="dialog.template" v-model="dialog.data"></component>
            </v-card-text>
            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn
                color="primary"
                @click="() => {
                  dialog.actions.ok.action(dialog.data);
                  if (dialog.actions.ok.close !== false) {
                    closeDialog(dialog.id);
                  }
                }"
              >
                {{ dialog.actions.ok.title || 'OK' }}
              </v-btn>
              <v-btn
                v-if="dialog.actions.cancel"
                text
                @click="() => {
                  if (dialog.actions.cancel.action) {
                    dialog.actions.cancel.action(dialog.data);
                  }
                  closeDialog(dialog.id);
                }"
              >
                {{ dialog.actions.cancel.title || 'Cancel' }}
              </v-btn>
            </v-card-actions>
          </v-card>
          <v-card v-else flat tile>
            <v-card-text>
              <div style="display: flex; justify-content: center; align-items: center; height: 100px;">
                <v-progress-circular indeterminate color="primary" size="40" />
              </div>
            </v-card-text>
          </v-card>
        </v-dialog>
      </template>

      <router-view />
    </v-app>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@2.5.10/dist/vuetify.js"></script>

  <script>
    const crud = (op, sheetName, params) => {
      const db = JSON.parse(localStorage.getItem('localDB')) || {};
      if (!db[sheetName]) db[sheetName] = { headers: [], rows: [] };
      const sheet = db[sheetName];
      const res = { status: '', data: [], message: '' };

      if (op === 'create') {
        JSON.parse(params).forEach(d => {
          if (!sheet.headers.length) sheet.headers = Object.keys(d);
          sheet.rows.push(sheet.headers.map(h => d[h] || ''));
        });
        localStorage.setItem('localDB', JSON.stringify(db));
        res.status = 'success';
      } else if (op === 'read') {
        res.data = sheet.rows.map(row => sheet.headers.reduce((o, h, i) => (o[h] = row[i], o), {}));
        res.status = 'success';
      }
      return JSON.stringify(res);
    };

    const Dashboard = {
      template: `<v-container>
        <h2>Dashboard</h2>
        <p>Selamat datang di dashboard.</p>
      </v-container>`
    };

    const ListView = {
      data: () => ({ items: [], username: '' }),
      created() {
        const user = localStorage.getItem('aktif');
        this.username = user;
        const key = `items-${user}`;
        if (!JSON.parse(crud("read", key)).data.length)
          crud("create", key, JSON.stringify([{ name: "Item A", email: "" }]));
        this.items = JSON.parse(crud("read", key)).data;
      },
      methods: {
        async editItem(item) {
          const editDialogId = this.$root.showDialog({
            type: 'form',
            title: 'Edit Data',
            content: `
              <v-text-field label="Nama" v-model="value.name"></v-text-field>
              <v-text-field label="Email" v-model="value.email"></v-text-field>
            `,
            data: { name: item.name, email: item.email },
            actions: {
              ok: {
                title: 'Simpan',
                close: false,
                action: async (data) => {
                  const confirm = await new Promise(resolve => {
                    const confirmDialogId = this.$root.showDialog({
                      type: 'info',
                      title:'Simpan',
                      content: 'Yakin ingin menyimpan perubahan?',width: '300px',
                      actions: {
                        ok: {
                          title: 'Ya',
                          action: () => {
                            this.$root.closeDialog(confirmDialogId);
                            resolve(true);
                          }
                        },
                        cancel: {
                          title: 'Tidak',
                          action: () => {
                            this.$root.closeDialog(confirmDialogId);
                            resolve(false);
                          }
                        }
                      }
                    });
                  });

                  if (confirm) {
                    const loadingDialogId = this.$root.showDialog({
                      type: 'loading',
                      content: '',
                      width: '100px',
                      persistent: true,
                      actions: { ok: { show: false, action: () => {} } }
                    });

                    try {
                      await new Promise(resolve => setTimeout(resolve, 1000));
                      const key = `items-${this.username}`;
                      const all = JSON.parse(crud("read", key)).data;
                      const index = all.findIndex(x => x.name === item.name);
                      if (index !== -1) {
                        all[index] = data;
                        localStorage.setItem('localDB', JSON.stringify({
                          ...JSON.parse(localStorage.getItem('localDB')),
                          [key]: {
                            headers: ['name', 'email'],
                            rows: all.map(d => [d.name, d.email])
                          }
                        }));
                        this.items = all;
                        this.$root.showSnackbar("Data berhasil disimpan!", "success");
                      }
                    } finally {
                      this.$root.closeDialog(loadingDialogId);
                      this.$root.closeDialog(editDialogId);
                    }
                  }
                }
              },
              cancel: {
                title: 'Batal',
                action: () => {
                  this.$root.showSnackbar("Edit dibatalkan", "info");
                }
              }
            }
          });
        }
      },
      template: `<v-container>
        <v-simple-table dense>
          <thead><tr><th>Nama</th></tr></thead>
          <tbody>
            <tr v-for="(item, i) in items" :key="i" @click="editItem(item)">
              <td>{{ item.name }}</td>
            </tr>
          </tbody>
        </v-simple-table>
      </v-container>`
    };

    const Login = {
      data: () => ({ isRegister: false, user: { username: '', password: '' } }),
      methods: {
        toggleMode() { this.isRegister = !this.isRegister },
        submit() {
          const { username, password } = this.user;
          if (!username || !password) {
            this.$root.showSnackbar('Username dan password harus diisi','warning');
            return;
          }

          const users = JSON.parse(crud("read", "users")).data;
          if (this.isRegister) {
            if (users.find(u => u.username === username)) {
              this.$root.showSnackbar('Username sudah terdaftar!','warning');
              return;
            }
            crud("create", "users", JSON.stringify([this.user]));
            this.$root.showSnackbar('Registrasi berhasil', 'success');
            this.toggleMode();
          } else {
            const match = users.find(u => u.username === username && u.password === password);
            if (match) {
              localStorage.setItem('aktif', username);
              this.$root.showSnackbar('Login berhasil', 'success');
              this.$router.push("/app/dashboard");
            } else {
              this.$root.showSnackbar('Login Gagal, username/password salah!', 'error');
            }
          }
        }
      },
      template: `<v-container fill-height>
        <v-row align="center" justify="center">
          <v-col cols="12" md="10">
            <v-card class="mx-auto" :style="{ maxWidth: '290px' }">
              <v-toolbar dark  color="primary">
                <v-toolbar-title>{{ isRegister ? 'Register' : 'Login' }}</v-toolbar-title>
              </v-toolbar>  
              <v-card-text>
                <v-text-field v-model="user.username" label="Username" />
                <v-text-field v-model="user.password" label="Password" type="password" />
              </v-card-text>
              <v-card-actions>
                <v-btn color="primary" @click="submit">{{ isRegister ? 'Register' : 'Login' }}</v-btn>
                <v-spacer></v-spacer>
                <v-btn text @click="toggleMode">
                  {{ isRegister ? 'Sudah punya akun?' : 'Belum punya akun?' }}
                </v-btn>
              </v-card-actions>
            </v-card>
          </v-col>
        </v-row>
      </v-container>`
    };

    const appChildRoutes = [
      { path: 'dashboard', title: 'Dashboard', icon: 'mdi-view-dashboard', component: Dashboard },
      { path: 'list', title: 'List View', icon: 'mdi-format-list-bulleted', component: ListView }
    ];

    const AppLayout = {
      data: () => ({ drawer: false, links: appChildRoutes }),
      computed: {
        currentTitle() {
          return (this.links.find(r => '/app/' + r.path === this.$route.path) || {}).title || "My App";
        }
      },
      methods: {
        go(path) {
          const full = '/app/' + path;
          if (this.$route.path !== full) this.$router.push(full);
          this.drawer = false;
        },
        logout() {
          localStorage.removeItem('aktif');
          this.$root.showSnackbar("Berhasil logout", "info");
          this.$router.push('/');
        }
      },

      template: `<v-app>
        <v-navigation-drawer app v-model="drawer">
          <v-list dense>
            <v-list-item v-for="r in links" :key="r.path" link @click="go(r.path)">
              <v-list-item-icon><v-icon>{{ r.icon }}</v-icon></v-list-item-icon>
              <v-list-item-title>{{ r.title }}</v-list-item-title>
            </v-list-item>
            <v-divider></v-divider>
            <v-list-item link @click="logout">
              <v-list-item-icon><v-icon>mdi-logout</v-icon></v-list-item-icon>
              <v-list-item-title>Logout</v-list-item-title>
            </v-list-item>
          </v-list>

        </v-navigation-drawer>
        <v-app-bar app color="primary" dark>
          <v-app-bar-nav-icon @click="drawer = !drawer" />
          <v-toolbar-title>{{ currentTitle }}</v-toolbar-title>
        </v-app-bar>
        <v-main><router-view /></v-main>
      </v-app>`
    };

    const router = new VueRouter({
      routes: [
        { path: '/', component: Login },
        { path: '/app', component: AppLayout, children: appChildRoutes }
      ]
    });

    router.beforeEach((to, from, next) => {
      const loggedIn = localStorage.getItem('aktif');
      if (to.path.startsWith('/app') && !loggedIn) {
        next('/');
      } else {
        next();
      }
    });

    new Vue({
      el: '#app',
      vuetify: new Vuetify(),
      router,
      data: () => ({
        dialogs: [],
        snackbar: {
          show: false,
          text: '',
          color: 'success',
          timeout: 3000
        },
        defaultDialog: {
          type: '',
          title: '',
          template: null,
          content: null,
          data: null,
          width: '500px',
          persistent: true,
          actions: {
            ok: { title: 'OK', action: () => {} },
            cancel: null
          }
        }
      }),
      created() {
        localStorage.removeItem('aktif');
      },
      methods: {
        showDialog(typeOrOptions, title = '', content = '') {
          let options = {};
          if (typeof typeOrOptions === 'string') {
            options = { type: typeOrOptions, title, content };
          } else {
            options = typeOrOptions;
          }

          const dialog = {
            ...this.defaultDialog,
            ...options,
            id: 'dialog-' + Date.now() + Math.random().toString(36).substr(2, 5),
            show: true
          };

          if (options.type === 'form') {
            const componentId = 'dynamic-form-' + dialog.id;
            Vue.component(componentId, {
              props: ['value'],
              template: `<div>${options.content}</div>`
            });
            dialog.template = componentId;
          } else if (options.type === 'loading') {
            const componentId = 'loading-dialog-' + dialog.id;
            Vue.component(componentId, {
              template: `
                <div style="display: flex; justify-content: center; align-items: center; height: 100px;">
                  <v-progress-circular indeterminate color="primary" size="40" />
                </div>
              `
            });
            dialog.template = componentId;
          }

          this.dialogs.push(dialog);
          return dialog.id;
        },
        closeDialog(id) {
          this.dialogs = this.dialogs.filter(dialog => dialog.id !== id);
        },
        showSnackbar(text, color = 'success') {
          this.snackbar.text = text;
          this.snackbar.color = color;
          this.snackbar.show = true;
        }
      }
    });
  </script>
</body>

</html>