feat: add files

This commit is contained in:
Ron 2025-02-16 14:10:19 +02:00
commit fbba5a6814
21 changed files with 14376 additions and 0 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
/.nuxt
/.output
/.ignore
/node_modules
.gitignore
README.md

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
.output
.data
.nuxt
.nitro
.cache
dist
node_modules
logs
*.log
.DS_Store
.fleet
.idea
.env
.env.*
!.env.example

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
ARG NODE_VERSION=20.10.0
ARG PORT=3000
FROM node:${NODE_VERSION}-slim as base
ENV NODE_ENV=production
ENV NODE_OPTIONS=--max-old-space-size=8192
WORKDIR /app
FROM base as build
COPY package.json .
COPY package-lock.json .
RUN npm install --production=false
COPY . .
RUN npm run build
RUN npm prune
FROM base as production
ENV PORT=$PORT
COPY --from=build /app/.output /app/.output
CMD ["node", ".output/server/index.mjs"]

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# Client Portal
## Commands
- `npm run dev` to start development
## Tech Stack
- Node.js
- Nuxt
- Directus
- Vuetify

4
app.vue Normal file
View File

@ -0,0 +1,4 @@
<template>
<NuxtPwaManifest />
<NuxtPage />
</template>

View File

@ -0,0 +1,14 @@
export default defineNuxtRouteMiddleware(async () => {
const { fetchUser, setUser } = useDirectusAuth();
const user = useDirectusUser();
if (!user.value) {
const user = await fetchUser();
setUser(user.value);
}
if (!user.value) {
return navigateTo("/login");
}
});

40
nuxt.config.ts Normal file
View File

@ -0,0 +1,40 @@
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
devtools: { enabled: true },
modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"],
app: {
head: {
titleTemplate: "%s • Port Nimara Client Portal",
title: "Port Nimara Client Portal",
meta: [
{ property: "og:title", content: "Port Nimara Client Portal" },
{ property: "og:image", content: "/og-image.png" },
{ name: "twitter:card", content: "summary_large_image" },
],
htmlAttrs: {
lang: "en",
},
},
},
runtimeConfig: {
public: {
directus: {
url: "https://contenthub.portnimara.com",
},
},
},
vuetify: {
vuetifyOptions: {
theme: {
defaultTheme: "portnimara",
themes: {
portnimara: {
colors: {
primary: "#387bca",
},
},
},
},
},
},
});

13983
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@vite-pwa/nuxt": "^0.10.6",
"nuxt": "^3.15.4",
"nuxt-directus": "^5.7.0",
"vue": "latest",
"vue-router": "latest",
"vuetify-nuxt-module": "^0.18.3"
}
}

77
pages/dashboard.vue Normal file
View File

@ -0,0 +1,77 @@
<template>
<v-app full-height>
<v-navigation-drawer>
<v-img src="/logo.jpg" height="75" class="my-6" />
<v-list color="primary" lines="two">
<v-list-item
to="/dashboard/site"
title="Site Analytics"
prepend-icon="mdi-view-dashboard"
/>
<v-list-item
to="/dashboard/data"
title="Data Analytics"
prepend-icon="mdi-finance"
/>
</v-list>
<template #append>
<v-list lines="two">
<v-list-item
@click="logOut"
title="Log out"
prepend-icon="mdi-logout"
base-color="error"
/>
</v-list>
</template>
</v-navigation-drawer>
<v-app-bar v-if="mdAndDown" elevation="2">
<template #prepend>
<v-img src="/logo.jpg" width="75" class="ml-3" />
</template>
<template #append>
<v-btn
@click="logOut"
class="mr-3"
variant="tonal"
color="error"
icon="mdi-logout"
/>
</template>
</v-app-bar>
<v-main>
<router-view />
<v-bottom-navigation :active="mdAndDown" color="primary" elevation="2">
<v-btn to="/dashboard/site">
<v-icon icon="mdi-view-dashboard" />
<span>Site Analytics</span>
</v-btn>
<v-btn to="/dashboard/data">
<v-icon icon="mdi-finance" />
<span>Data Analytics</span>
</v-btn>
</v-bottom-navigation>
</v-main>
</v-app>
</template>
<script setup>
definePageMeta({
middleware: ["authentication"],
layout: false,
});
const { mdAndDown } = useDisplay();
const { logout } = useDirectusAuth();
const logOut = async () => {
await logout();
return navigateTo("/login");
};
</script>

31
pages/dashboard/data.vue Normal file
View File

@ -0,0 +1,31 @@
<template>
<div class="embed">
<iframe
src="https://flows.portnimara.com/public/dashboards/ugQabIMShSld7pXWYLwpsV1kpbSILPiui5eIOfhd?org_slug=default"
/>
</div>
</template>
<script lang="ts" setup>
useHead({
title: "Data Analytics",
});
</script>
<style scoped>
.embed {
position: relative;
overflow: hidden;
width: 100%;
height: 100vh;
}
.embed iframe {
position: absolute;
width: 100%;
height: calc(100% + 180px);
border: 0;
top: 0;
left: 0;
}
</style>

View File

@ -0,0 +1,5 @@
<script lang="ts" setup>
definePageMeta({
redirect: "/dashboard/site",
});
</script>

30
pages/dashboard/site.vue Normal file
View File

@ -0,0 +1,30 @@
<template>
<div class="embed">
<iframe
src="https://analytics.portnimara.com/share/56Dc1w6yYGAOjyoj/portnimara.com"
/>
</div>
</template>
<script lang="ts" setup>
useHead({
title: "Site Analytics",
});
</script>
<style scoped>
.embed {
position: relative;
overflow: hidden;
width: 100%;
height: 100vh;
}
.embed iframe {
width: 100%;
height: calc(100% + 120px);
border: 0;
position: absolute;
top: -120px;
}
</style>

5
pages/index.vue Normal file
View File

@ -0,0 +1,5 @@
<script setup>
definePageMeta({
redirect: "/dashboard",
});
</script>

113
pages/login.vue Normal file
View File

@ -0,0 +1,113 @@
<template>
<v-app full-height>
<v-main class="container">
<v-container class="fill-height" fluid>
<v-row align="center" justify="center" class="fill-height">
<v-card class="pa-6" rounded="xl" max-width="350" elevation="2">
<v-form @submit.prevent="submit" v-model="valid">
<v-row no-gutters>
<v-col cols="12">
<v-img src="/logo.jpg" width="200" class="mb-3 mx-auto" />
</v-col>
<v-scroll-y-transition>
<v-col v-if="errorThrown" cols="12" class="my-3">
<v-alert
text="Invalid email address or password"
color="error"
variant="tonal"
/>
</v-col>
</v-scroll-y-transition>
<v-col cols="12">
<v-row dense>
<v-col cols="12" class="mt-4">
<v-text-field
v-model="emailAddress"
placeholder="Email address"
:disabled="loading"
:rules="[
(value) => !!value || 'Must not be empty',
(value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ||
'Invalid email address',
]"
variant="outlined"
type="email"
autofocus
/>
</v-col>
<v-col cols="12">
<v-text-field
@click:append-inner="passwordVisible = !passwordVisible"
v-model="password"
placeholder="Password"
:disabled="loading"
:type="passwordVisible ? 'text' : 'password'"
:append-inner-icon="
passwordVisible ? 'mdi-eye' : 'mdi-eye-off'
"
:rules="[(value) => !!value || 'Must not be empty']"
autocomplete="current-password"
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-btn
text="Log in"
:disabled="!valid"
:loading="loading"
type="submit"
variant="tonal"
color="primary"
size="large"
block
/>
</v-col>
</v-row>
</v-col>
</v-row>
</v-form>
</v-card>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script lang="ts" setup>
const { login } = useDirectusAuth();
const loading = ref(false);
const errorThrown = ref(false);
const emailAddress = ref();
const password = ref();
const passwordVisible = ref(false);
const valid = ref(false);
const submit = async () => {
try {
loading.value = true;
await login({ email: emailAddress.value, password: password.value });
return navigateTo("/dashboard");
} catch (error) {
errorThrown.value = true;
} finally {
loading.value = false;
}
};
useHead({
title: "Login",
});
</script>
<style>
.container {
background: url(/background.jpg);
background-repeat: no-repeat;
background-size: cover;
}
</style>

BIN
public/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
public/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
public/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
static/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

3
tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}