1
0
Fork 0

Get login working.

This commit is contained in:
Brian D 2024-11-24 20:50:39 -05:00
parent 865a070826
commit 4728c1765b
10 changed files with 343 additions and 70 deletions

View file

@ -4,6 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"server": "node server.js",
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",

23
server.js Normal file
View file

@ -0,0 +1,23 @@
import http from "http";
const host = 'localhost';
const port = 8000;
const user = JSON.stringify({
name: "Joe User",
});
const requestListener = function (req, res) {
res.setHeader("Content-Type", "application/json");
switch (req.url) {
case "/login":
res.writeHead(200);
res.end(user);
break
}
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});

View file

@ -1,18 +1,57 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref} from "vue";
import Avatar from 'primevue/avatar';
import Menu from "primevue/menu";
import { userActions } from "@/stores/userActionsStore";
import { useUserStore } from "@/stores/userStore";
import Navigation from "@/components/Navigation.vue"; import Navigation from "@/components/Navigation.vue";
const userStore = useUserStore();
const logout = () => {
userStore.logout();
};
// Attach handlers.
userActions.map((item) => {
if (item.cmd === 'logout') {
item.command = logout;
}
});
const items = ref(userActions);
const menu = ref();
const toggle = (event) => { menu.value.toggle(event); };
</script> </script>
<template> <template>
<aside> <aside>
<Navigation></Navigation> <Navigation></Navigation>
<div class="mt-auto"> <div class="mt-auto act-avatar">
<hr class="mb-4 mx-4 border-t border-0 border-surface-800" /> <a class="nav-item" @click="toggle">
<a class="m-4 nav-item"> <div v-if="userStore.isAnonymous">
<img <Avatar icon="pi pi-user" size="large" shape="circle" />
alt="user avatar" </div>
src="https://fqjltiegiezfetthbags.supabase.co/storage/v1/render/image/public/block.images/blocks/avatars/circle/avatar-f-1.png" class="mr-2 lg:mr-0 w-8 h-8" /> <div v-else>
<span class="font-medium inline lg:hidden">Amy Elsner</span> <Avatar v-bind:label="userStore.initials" size="large" shape="circle" />
</div>
<span class="inline lg:hidden">{{ userStore.user.name }}</span>
</a> </a>
<Menu ref="menu" id="overlay_menu" :model="items" :popup="true">
<template #item="{ item, props }">
<a
class="flex items-center"
v-bind:class="{ 'act-useraction': item.needsAuth }"
v-bind="props.action"
v-if="!item.needsAuth || !userStore.isAnonymous">
<span v-bind:class="item.icon" />
<span>{{ item.label }}</span>
</a>
</template>
</Menu>
</div> </div>
</aside> </aside>
</template> </template>
@ -24,10 +63,16 @@ aside {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100%; height: 100%;
.act-avatar {
display: none;
}
} }
@media (screen(sm)) { @media (screen(sm)) {
aside { aside {
flex-direction: column; flex-direction: column;
.act-avatar {
display: block;
}
} }
} }
</style> </style>

View file

@ -1,77 +1,104 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "@/stores/userStore";
import { userActions } from "@/stores/userActionsStore";
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import OverlayBadge from "primevue/overlaybadge";
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import Button from 'primevue/button';
import Menu from "primevue/menu";
const route = useRoute(); const route = useRoute();
const page_title = ref(); const page_title = ref();
const userStore = useUserStore();
watch(() => route.meta, (newVal, oldVal) => { watch(() => route.meta, (newVal, oldVal) => {
page_title.value = newVal.title; page_title.value = newVal.title;
}) })
const toggle = (event) => {
menu.value.toggle(event);
};
const menu = ref();
const localActions = [{
icon: 'pi pi-question',
label: 'About',
needsAuth: false,
}, {
icon: 'pi pi-question',
label: 'AboutFoo',
needsAuth: true,
}];
const allActions = [...userActions, ...localActions];
const items = ref(allActions);
</script> </script>
<template> <template>
<header class="flex justify-between items-center px-4"> <header class="flex justify-between items-center px-4">
<div class="flex"> <div class="flex">
<svg class="Icon" style="height: 32px; width: 32px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g class="" style="" transform="translate(0,0)">
<path d="M329.8 235.69l62.83-82.71 42.86 32.56-62.83 82.75zm-12.86-9.53l66.81-88-45-34.15-66.81 88zm-27.48-97.78l-19.3 39.57 57-75-42.51-32.3-36.24 47.71zm-20.74-73.24l-46.64-35.43-42 55.31 53.67 26.17zm107 235.52l-139-102.71-9.92.91 4.56 2 62.16 138.43-16.52 2.25-57.68-128.5-40-17.7-4-30.84 39.41 19.42 36.36-3.33 17-34.83-110.9-54.09-80.68 112.51L177.6 346.67l-22.7 145.62H341V372.62l35.29-48.93L387 275.77z" fill="#000" fill-opacity="1">
</path>
</g>
</svg>
<span class="title">{{ page_title }}</span> <span class="title">{{ page_title }}</span>
</div> </div>
<a <div class="flex gap-2">
v-styleclass="{ <Button
selector: '@next', as="router-link"
enterFromClass: 'hidden', to="/login"
enterActiveClass: 'animate-fadein', v-if="userStore.isAnonymous">
leaveToClass: 'hidden', <i class="pi pi-sign-in" />
leaveActiveClass: 'animate-fadeout', <span>Login</span>
hideOnOutsideClick: true </Button>
}" <Button type="button" icon="pi pi-ellipsis-v" @click="toggle" aria-haspopup="true" aria-controls="overlay_menu" />
class="cursor-pointer block lg:hidden text-surface-700 dark:text-surface-100" <Menu ref="menu" id="overlay_menu" :model="items" :popup="true">
> <template #start>
<i class="pi pi-ellipsis-v text-2xl" /> <span class="inline-flex items-center gap-1 px-2 py-2">
</a> <svg class="Icon" style="height: 32px; width: 32px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<ul <g class="" style="" transform="translate(0,0)">
class="list-none p-0 m-0 hidden lg:flex lg:items-center select-none lg:flex-row bg-surface-0 dark:bg-surface-950 border lg:border-0 border-surface right-0 top-full z-10 shadow lg:shadow-none absolute lg:static" <path d="M329.8 235.69l62.83-82.71 42.86 32.56-62.83 82.75zm-12.86-9.53l66.81-88-45-34.15-66.81 88zm-27.48-97.78l-19.3 39.57 57-75-42.51-32.3-36.24 47.71zm-20.74-73.24l-46.64-35.43-42 55.31 53.67 26.17zm107 235.52l-139-102.71-9.92.91 4.56 2 62.16 138.43-16.52 2.25-57.68-128.5-40-17.7-4-30.84 39.41 19.42 36.36-3.33 17-34.83-110.9-54.09-80.68 112.51L177.6 346.67l-22.7 145.62H341V372.62l35.29-48.93L387 275.77z" fill="#000" fill-opacity="1">
> </path>
<li> </g>
<a> </svg>
<i class="pi pi-inbox mr-2 lg:mr-0" /> <span class="text-xl font-semibold">ACTIVISM<span class="text-primary">APP</span></span>
<span class="block lg:hidden font-medium">Inbox</span> </span>
</a> </template>
</li> <template #item="{ item, props }">
<li> <a
<a> class="flex items-center"
<OverlayBadge severity="danger" class="mr-2 lg:mr-0"> v-bind:class="{ 'act-useraction': item.needsAuth }"
<i class="pi pi-bell" /> v-bind="props.action"
</OverlayBadge> v-if="!item.needsAuth || !userStore.isAnonymous">
<span class="block lg:hidden font-medium">Notifications</span> <span v-bind:class="item.icon" />
</a> <span>{{ item.label }}</span>
</li> </a>
<li class="border-t border-surface lg:border-t-0"> </template>
<a class="flex p-4 lg:px-4 lg:py-2 items-center hover:bg-surface-100 dark:hover:bg-surface-700 font-medium rounded-border cursor-pointer duration-150 transition-colors"> </Menu>
<img </div>
alt="user avatar"
src="https://fqjltiegiezfetthbags.supabase.co/storage/v1/render/image/public/block.images/blocks/avatars/circle/avatar-f-1.png" class="mr-4 lg:mr-0 w-8 h-8" />
<div class="block lg:hidden">
<div class="text-surface-900 dark:text-surface-0 font-medium">Josephine Lillard</div>
<span class="text-surface-600 dark:text-surface-200 font-medium text-sm">Marketing Specialist</span>
</div>
</a>
</li>
</ul>
</header> </header>
</template> </template>
<style scoped> <style lang="scss" scoped>
.title {
font-size: 160%;
}
header { header {
background-color: white; background-color: white;
border-bottom: 1px solid #BBB; border-bottom: 1px solid #BBB;
li a { .title {
@apply flex p-4 lg:px-4 lg:py-2 items-center text-surface-600 dark:text-surface-200 hover:text-surface-900 dark:hover:text-surface-0 hover:bg-surface-100 dark:hover:bg-surface-700 font-medium rounded-border cursor-pointer duration-150 transition-colors; font-size: 160%;
i { }
@apply text-base lg:!text-2xl leading-none; }
} .act-menubar {
border: none !important;
//width: 10em;
}
.act-menubar-end {
margin-left: 0 !important;
}
@media (screen(sm)) {
.act-useraction {
background-color: red;
display: none; // sidebar will have them
} }
} }
</style> </style>

View file

@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import AboutView from '../views/AboutView.vue' import AboutView from '../views/AboutView.vue'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import ProfileView from '../views/ProfileView.vue' import ProfileView from '../views/ProfileView.vue'
import StickerView from '../views/StickerView.vue' import StickerView from '../views/StickerView.vue'
@ -39,6 +40,14 @@ const router = createRouter({
name: 'sticker', name: 'sticker',
path: '/sticker', path: '/sticker',
}, },
{
component: LoginView,
meta: {
title: 'Login',
},
name: 'login',
path: '/login',
},
], ],
}) })

View file

@ -0,0 +1,18 @@
export const userActions = [{
icon: 'pi pi-inbox',
label: 'Inbox',
needsAuth: true,
}, {
icon: 'pi pi-bell',
label: 'Notifications',
needsAuth: true,
}, {
icon: 'pi pi-cog',
label: 'Settings',
needsAuth: true,
}, {
cmd: 'logout',
icon: 'pi pi-sign-out',
label: 'Logout',
needsAuth: true,
}];

48
src/stores/userStore.ts Normal file
View file

@ -0,0 +1,48 @@
import { defineStore } from 'pinia'
const anonymous = {
name: 'Anonymous',
username: null,
};
export const useUserStore = defineStore('user', {
actions: {
async login(username: string, password: string) {
const response = await fetch(
"/login",
{
method: 'POST',
},
);
const json = await response.json();
// this.user.name = json.name;
this.user = {
name: json.name,
username: "username",
}
return json;
},
logout() {
console.log('logout2');
// this.user = anonymous;
// this.hasChanged = true;
// this.user.name = 'Anonymous';
this.$reset();
// this.$patch({user: anonymous});
},
},
getters: {
initials: (state): string => {
return state.user.name[0] || '';
},
isAnonymous: (state): boolean => {
return state.user.name === 'Anonymous';
},
},
state: () => {
console.log('reset state');
return {
user: anonymous,
};
},
})

86
src/views/LoginView.vue Normal file
View file

@ -0,0 +1,86 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import Button from 'primevue/button';
import { Form } from '@primevue/forms';
import InputText from 'primevue/inputtext';
import Message from 'primevue/message';
import Toast from 'primevue/toast';
import { useUserStore } from "@/stores/userStore";
const userStore = useUserStore();
const username = ref();
const password = ref();
const router = useRouter();
function handleLogin({ valid }) {
if (!valid) {
return;
}
userStore.login(username.value, username.value).then((data) => {
router.push('/88ae126f-b19a-4287-abe0-a8f5ac763cb7');
});
}
const initialValues = reactive({
username: 'joe',
password: 'password',
});
const resolver = ({ values }) => {
const errors = {};
if (!values.username) {
errors.username = [{message: 'Username is required.'}];
}
if (!values.password) {
errors.password = [{message: 'Password is required.'}];
}
return { errors }
};
</script>
<template>
<div class="login-form">
<Toast/>
<Form :initialValues :resolver @submit="handleLogin" v-slot="$form">
<div>
<label for="username">Username</label>
<InputText name="username" v-model="username" id="username" type="text" />
<Message v-if="$form.username?.invalid" severity="error" size="small" variant="simple">
{{ $form.username.error?.message }}
</Message>
</div>
<div>
<label for="password">Password</label>
<InputText name="password" v-model="password" id="password" type="password" />
<Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">
{{ $form.password.error?.message }}
</Message>
</div>
<div>
<label>&nbsp;</label>
<Button type="submit">Login</Button>
</div>
</Form>
</div>
</template>
<style lang="scss" scoped>
.login-form {
display: flex;
justify-content: center;
form {
display: flex;
flex-direction: column;
font-size: 120%;
margin: 20px;
gap: 10px;
> div {
padding: 4px;
display: flex;
flex-direction: column;
}
}
}
</style>

View file

@ -9,11 +9,13 @@ import Panel from 'primevue/panel';
import Sticker from "@/components/Sticker.vue"; import Sticker from "@/components/Sticker.vue";
import Timeline from 'primevue/timeline'; import Timeline from 'primevue/timeline';
import {useRoute} from 'vue-router'; import {useRoute} from 'vue-router';
import { useUserStore } from "@/stores/userStore";
const userStore = useUserStore();
const route = useRoute(); const route = useRoute();
const uuid = ref('uuid') const uuid = ref<string>(route.params.uuid as string);
uuid.value = route.params.uuid;
const first = computed(() => { const first = computed(() => {
const parts = uuid.value.split('-') const parts = uuid.value.split('-')
@ -68,7 +70,7 @@ const events = [{
</div> </div>
</Panel> </Panel>
<Panel class="bg-white" header="Stickers"> <Panel class="bg-white" header="Stickers">
<Button as="router-link" to="/sticker">New sticker</Button> <Button as="router-link" to="/sticker" v-if="!userStore.isAnonymous">New sticker</Button>
<div id="StickerWrapper"> <div id="StickerWrapper">
<Sticker color="#000088" layout="layout1" message1="I support M4A" /> <Sticker color="#000088" layout="layout1" message1="I support M4A" />
</div> </div>
@ -76,8 +78,8 @@ const events = [{
<Panel header="Metrics"> <Panel header="Metrics">
<MeterGroup :value="meterValue" /> <MeterGroup :value="meterValue" />
</Panel> </Panel>
<div class="grid sm:grid-cols-2"> <div class="grid sm:grid-cols-2 gap-2">
<div class="flex"> <div id=1 class="flex">
<Panel header="Activities"> <Panel header="Activities">
<Timeline <Timeline
align="left" align="left"
@ -108,8 +110,8 @@ const events = [{
</Timeline> </Timeline>
</Panel> </Panel>
</div> </div>
<div class="flex items-center justify-center"> <div id=2 class="flex items-center justify-center">
<Panel class="m-4" header="Tags"> <Panel header="Tags">
<Chip label="labor" icon="pi pi-wrench" /> <Chip label="labor" icon="pi pi-wrench" />
<Chip label="feminism" icon="pi pi-venus" /> <Chip label="feminism" icon="pi pi-venus" />
<Chip label="environmental" icon="pi pi-globe" /> <Chip label="environmental" icon="pi pi-globe" />
@ -126,9 +128,13 @@ const events = [{
<style scoped> <style scoped>
#StickerWrapper { #StickerWrapper {
background-color: #F2F2F2;
padding: 5px;
width: 300px; width: 300px;
} }
.profilePage > div { .profilePage {
@apply m-4; @apply p-2 gap-2;
display: flex;
flex-direction: column;
} }
</style> </style>

View file

@ -8,11 +8,21 @@ import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
vueDevTools(), // vueDevTools(),
], ],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
}, },
}, },
server: {
proxy: {
'/login': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false,
// rewrite: (path) => path.replace(/^\/api/, '')
},
},
},
}) })