1
0
Fork 0

Compare commits

..

10 commits

Author SHA1 Message Date
3be0755074 Get sticker data from the API backend.
All checks were successful
/ test (push) Successful in 4m47s
Store the JWT access token and use it later to get the sticker.

Add missing url property of stickers.
2025-10-25 10:51:56 -04:00
929f22abeb Upgrade dependencies.
Use user_id in places we were using username.  Still debating this.

Start getting this working with API backend, until there were calls to
/api/sticker which don't exist in the API yet.  Then I rediscovered the
local json-server.

So now there are 2 api backends.  We'll incrementally migrate from the
first version to the second.

SCSS and tailwind weren't working for nav-items.css.  The browser just
saw the raw version.  So for now, just inlined that.  Probably these
nav items will become components (if they are shared).
2025-10-22 09:19:37 -04:00
5b48c5a8c5 Resolve issues from 'npm run build'.
One of them needed a patch in primevue 4.4.2 which was release only 2 weeks ago.
2025-10-19 00:56:36 -04:00
47ed7af853 Drop default and rename demo. 2024-12-26 12:30:10 -05:00
9c98afc27c Add demo workflow. 2024-12-26 12:16:54 -05:00
1f68142640 Add default workflow. 2024-12-26 12:10:39 -05:00
044c21840c Live QR Codes 2024-12-23 16:14:22 -05:00
49b0a962a5 Add print view 2024-12-23 16:14:07 -05:00
86de4e5827 Create Neat layout and Print view. 2024-12-22 16:55:23 -05:00
cfc985ffb0 console.log 2024-12-22 14:20:44 -05:00
28 changed files with 4394 additions and 2418 deletions

View file

@ -0,0 +1,6 @@
on: [push]
jobs:
test:
runs-on: docker
steps:
- run: echo All Good

6219
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,40 +15,44 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@pinia/colada": "^0.13.0", "@pinia/colada": "^0.17.6",
"@primevue/forms": "^4.2.2", "@primevue/forms": "^4.2.2",
"@primevue/themes": "^4.2.2", "@primevue/themes": "^4.4.1",
"pinia": "^2.2.6", "@tailwindcss/postcss": "^4.1.15",
"pinia": "^3.0.3",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.2.2", "primevue": "^4.4.1",
"tailwindcss-primeui": "^0.3.4", "tailwindcss-primeui": "^0.6.1",
"vue": "^3.5.12", "uqr": "^0.1.2",
"vue": "^3.5.22",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.1.15",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^27.0.0",
"@types/node": "^22.9.0", "@types/node": "^24.9.0",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^6.0.1",
"@vitest/eslint-plugin": "1.1.7", "@vitest/eslint-plugin": "1.3.23",
"@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.1.3", "@vue/eslint-config-typescript": "^14.1.3",
"@vue/language-server": "^3.1.1",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.14.0", "eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0", "eslint-plugin-vue": "^10.5.1",
"jsdom": "^25.0.1", "jsdom": "^27.0.1",
"json-server": "^1.0.0-beta.3", "json-server": "^1.0.0-beta.3",
"npm-run-all2": "^7.0.1", "npm-run-all2": "^8.0.4",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"sass-embedded": "^1.81.0", "sass-embedded": "^1.81.0",
"tailwindcss": "^3.4.15", "tailwindcss": "^4.1.15",
"typescript": "~5.6.3", "typescript": "~5.9.3",
"vite": "^5.4.10", "vite": "^7.1.11",
"vite-plugin-vue-devtools": "^7.5.4", "vite-plugin-vue-devtools": "^8.0.3",
"vitest": "^2.1.4", "vitest": "^3.2.4",
"vue-tsc": "^2.1.10" "vue-tsc": "^3.1.1"
} }
} }

View file

@ -1,6 +1,5 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, '@tailwindcss/postcss': {},
autoprefixer: {},
}, },
} }

View file

@ -1,78 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from 'vue'
import { RouterView } from 'vue-router' import { RouterView, useRoute } from 'vue-router'
import Footer from "@/components/Footer.vue"; import Default from '@/layouts/Default.vue'
import Sidebar from "@/components/Sidebar.vue";
import TopBar from "@/components/TopBar.vue";
import { useSessionStore } from "@/stores/sessionStore";
const sessionStore = useSessionStore(); const route = useRoute();
const sidebarSize = computed(() => { const layout = computed(() => {
return sessionStore.isAnonymous ? '0' : '90px'; const layoutComponent = route.meta.layout || Default;
return layoutComponent;
}); });
</script> </script>
<template> <template>
<div class="act-layout"> <component :is="layout">
<TopBar class="act-header"></TopBar> <router-view name="layout" />
<div class="act-sidebar" v-if="!sessionStore.isAnonymous"> </component>
<Sidebar></Sidebar>
</div>
<div class="act-content">
<main class="">
<RouterView />
</main>
<div class="act-footer">
<Footer></Footer>
</div>
</div>
</div>
</template> </template>
<style scoped lang="scss">
$header-height: 60px !default;
$sidebar-size: 90px !default;
.act-layout {
// To scroll only in the middle, see https://jsfiddle.net/VNVqs/
margin-bottom: v-bind(sidebarSize); // for scrolling
margin-top: $header-height; // for scrolling
.act-footer {
display: none;
}
.act-sidebar {
bottom: 0;
height: v-bind(sidebarSize);
left: 0;
position: fixed;
right: 0;
z-index: 20;
}
.act-header {
height: $header-height;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 20;
}
@media (screen(sm)) {
margin-bottom: 0;
margin-left: v-bind(sidebarSize);
.act-footer {
display: block
}
.act-sidebar {
left: 0;
height: 100%;
position: fixed;
width: v-bind(sidebarSize);
}
.act-header {
left: v-bind(sidebarSize);
}
}
}
</style>

View file

@ -1,3 +1,4 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @import "tailwindcss/preflight";
@tailwind utilities; @import "tailwindcss-primeui";
@import 'primeicons/primeicons.css';

View file

@ -1,14 +0,0 @@
.nav-item {
align-items: center;
display: flex;
flex-direction: column;
padding: 16px;
@apply hover:bg-surface-800 rounded-border text-surface-300 hover:text-white;
span {
@apply text-base;
text-align: center;
}
}
.nav-icon {
@apply text-base;
}

View file

@ -1,3 +1,5 @@
<!--Only displays for > sm screens -->
<script lang="ts" setup> <script lang="ts" setup>
</script> </script>
@ -8,13 +10,14 @@
</template> </template>
<style scoped> <style scoped>
@import "@/assets/nav-items.css"; @reference "tailwindcss";
footer { footer {
background-color: #E0E0E0; background-color: #E0E0E0;
color: #777; color: #777;
padding: 20px; padding: 20px;
} }
@media (screen(sm)) { @media (min-width: 640px) {
aside { aside {
flex-direction: column; flex-direction: column;
} }

View file

@ -3,16 +3,14 @@
<template> <template>
<nav> <nav>
<RouterLink <RouterLink class="nav-item" to="/">
class="nav-item"
to="/">
<i class="pi pi-home nav-icon" /> <i class="pi pi-home nav-icon" />
<span>Home</span> <span>Home</span>
</RouterLink> </RouterLink>
<RouterLink class="nav-item" to="/sticker"> <a class="nav-item">
<i class="pi pi-image nav-icon" /> <i class="pi pi-image nav-icon" />
<span>Sticker</span> <span>Sticker</span>
</RouterLink> </a>
<a class="nav-item"> <a class="nav-item">
<i class="pi pi-users nav-icon" /> <i class="pi pi-users nav-icon" />
<span>Team</span> <span>Team</span>
@ -21,9 +19,7 @@
<i class="pi pi-calendar nav-icon" /> <i class="pi pi-calendar nav-icon" />
<span>Events</span> <span>Events</span>
</a> </a>
<RouterLink <RouterLink class="nav-item" to="/profile">
class="nav-item"
to="/profile">
<i class="pi pi-user nav-icon" /> <i class="pi pi-user nav-icon" />
<span>Profile</span> <span>Profile</span>
</RouterLink> </RouterLink>
@ -31,12 +27,37 @@
</template> </template>
<style scoped> <style scoped>
@import "@/assets/nav-items.css"; @reference "tailwindcss";
@reference "tailwindcss-primeui";
nav { nav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
@media (screen(sm)) {
/* Duplicated in Sidebar.vue. */
.nav-item {
align-items: center;
display: flex;
flex-direction: column;
padding: 16px;
@apply hover:bg-surface-800 rounded-border text-surface-300 hover:text-white;
span {
@apply text-base;
text-align: center;
}
}
.nav-icon {
@apply text-base;
}
@media (min-width: 640px) {
nav { nav {
flex-direction: column; flex-direction: column;
} }

View file

@ -1,11 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import QRCodeImage from './QRCodeImage.vue'; import { defineProps } from 'vue';
import { renderSVG } from 'uqr';
const { content } = defineProps({
content: {
type: String,
required: true,
},
});
const svg = renderSVG(content);
</script> </script>
<template> <template>
<div class="qr-code"> <div class="qr-code">
<QRCodeImage class="qr-code-image" />
<!-- <span class="uuid">1337-9339</span>--> <!-- <span class="uuid">1337-9339</span>-->
<div :innerHTML="svg"></div>
</div> </div>
</template> </template>
@ -13,6 +24,7 @@ import QRCodeImage from './QRCodeImage.vue';
.qr-code { .qr-code {
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
background-color: white; background-color: white;
padding: 3px;
} }
.uuid { .uuid {
color: rgba(51, 51, 51, 0.91); color: rgba(51, 51, 51, 0.91);

View file

@ -41,6 +41,7 @@ const toggle = (event: Event) => { menu.value.toggle(event); };
<aside> <aside>
<Navigation></Navigation> <Navigation></Navigation>
<div class="mt-auto act-avatar"> <div class="mt-auto act-avatar">
<!-- TODO -->
<a class="nav-item" @click="toggle"> <a class="nav-item" @click="toggle">
<div v-if="sessionStore.isAnonymous"> <div v-if="sessionStore.isAnonymous">
<Avatar icon="pi pi-user" size="large" shape="circle" /> <Avatar icon="pi pi-user" size="large" shape="circle" />
@ -67,7 +68,9 @@ const toggle = (event: Event) => { menu.value.toggle(event); };
</template> </template>
<style scoped> <style scoped>
@import "@/assets/nav-items.css"; @reference "tailwindcss";
@reference "tailwindcss-primeui";
aside { aside {
background-color: #333; background-color: #333;
display: flex; display: flex;
@ -77,7 +80,20 @@ aside {
display: none; display: none;
} }
} }
@media (screen(sm)) { /* Duplicated in Navigation.vue. */
.nav-item {
align-items: center;
display: flex;
flex-direction: column;
padding: 16px;
@apply hover:bg-surface-800 rounded-border text-surface-300 hover:text-white;
span {
@apply text-base;
text-align: center;
}
}
@media (min-width: 640px) {
aside { aside {
flex-direction: column; flex-direction: column;
.act-avatar { .act-avatar {

View file

@ -5,11 +5,13 @@ import Image from 'primevue/image'
import { type Organization, useOrganization } from '@/queries/organization' import { type Organization, useOrganization } from '@/queries/organization'
const { const {
id,
layout, color, layout, color,
orgs, orgs,
message1, message2, message1, message2,
message3, message4, message3, message4,
} = defineProps([ } = defineProps([
'id',
'layout', 'color', 'layout', 'color',
"orgs", "orgs",
'message1', 'message2', 'message1', 'message2',
@ -39,7 +41,6 @@ const color2 = computed(() => {
const { state: allOrgs } = useOrganization(); const { state: allOrgs } = useOrganization();
const myOrgs = computed(() => { const myOrgs = computed(() => {
console.log(orgs)
if (allOrgs.value.status !== 'success') { if (allOrgs.value.status !== 'success') {
return []; return [];
} }
@ -47,10 +48,14 @@ const myOrgs = computed(() => {
return orgs.includes(org.id); return orgs.includes(org.id);
}); });
}); });
const qrContent = computed(() => {
return `https://anactivist.com/${id}`;
})
</script> </script>
<template> <template>
<div class="sticker"> <div class="sticker">
<QRCode class="qr-code"></QRCode> <QRCode class="qr-code" :content="qrContent"></QRCode>
<div class="content"> <div class="content">
<div class="orgs"> <div class="orgs">
<div v-for="org in myOrgs" :key="org.id"> <div v-for="org in myOrgs" :key="org.id">

View file

@ -133,7 +133,7 @@ header {
.act-menubar-end { .act-menubar-end {
margin-left: 0 !important; margin-left: 0 !important;
} }
@media (screen(sm)) { @media (min-width: 640px) {
.act-useraction { .act-useraction {
background-color: red; background-color: red;
display: none; // sidebar will have them display: none; // sidebar will have them

77
src/layouts/Default.vue Normal file
View file

@ -0,0 +1,77 @@
<script setup lang="ts">
import { computed } from "vue";
import { RouterView } from 'vue-router';
import Footer from "@/components/Footer.vue";
import Sidebar from "@/components/Sidebar.vue";
import TopBar from "@/components/TopBar.vue";
import { useSessionStore } from "@/stores/sessionStore";
const sessionStore = useSessionStore();
const sidebarSize = computed(() => {
return sessionStore.isAnonymous ? '0' : '90px';
});
</script>
<template>
<div class="act-layout">
<TopBar class="act-header"></TopBar>
<div class="act-sidebar" v-if="!sessionStore.isAnonymous">
<Sidebar></Sidebar>
</div>
<div class="act-content">
<main class="">
<router-view />
</main>
<div class="act-footer">
<Footer></Footer>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
$header-height: 60px !default;
$sidebar-size: 90px !default;
.act-layout {
// To scroll only in the middle, see https://jsfiddle.net/VNVqs/
margin-bottom: v-bind(sidebarSize); // for scrolling
margin-top: $header-height; // for scrolling
.act-footer {
display: none;
}
.act-sidebar {
bottom: 0;
height: v-bind(sidebarSize);
left: 0;
position: fixed;
right: 0;
z-index: 20;
}
.act-header {
height: $header-height;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 20;
}
@media (min-width: 640px) {
margin-bottom: 0;
margin-left: v-bind(sidebarSize);
.act-footer {
display: block
}
.act-sidebar {
left: 0;
height: 100%;
position: fixed;
width: v-bind(sidebarSize);
}
.act-header {
left: v-bind(sidebarSize);
}
}
}
</style>

9
src/layouts/Neat.vue Normal file
View file

@ -0,0 +1,9 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<main class="">
<router-view />
</main>
</template>

View file

@ -1,9 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useSessionStore } from "@/stores/sessionStore"; import { useSessionStore } from "@/stores/sessionStore";
import Neat from '../layouts/Neat.vue'
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 LoginView from '../views/LoginView.vue'
import PrintView from '../views/PrintView.vue'
import ProfileView from '../views/ProfileView.vue' import ProfileView from '../views/ProfileView.vue'
import StickerBuilderView from '../views/StickerBuilderView.vue' import StickerBuilderView from '../views/StickerBuilderView.vue'
import StickerView from '../views/StickerView.vue' import StickerView from '../views/StickerView.vue'
@ -31,13 +34,22 @@ const router = createRouter({
name: 'home', name: 'home',
path: '/', path: '/',
}, },
{
component: PrintView,
meta: {
layout: Neat,
title: 'Print',
},
name: 'print',
path: '/print/:user_id',
},
{ {
component: ProfileView, component: ProfileView,
meta: { meta: {
title: 'Profile', title: 'Profile',
}, },
name: 'profile', name: 'profile',
path: '/profile/:username?', path: '/profile/:user_id?',
}, },
{ {
component: StickerBuilderView, component: StickerBuilderView,

View file

@ -1,17 +1,16 @@
import {acceptHMRUpdate, defineStore} from 'pinia' import {acceptHMRUpdate, defineStore} from 'pinia'
import { useUserStore } from '@/stores/userStore' import { useUserStore } from '@/stores/userStore'
export interface User { export interface User {
name: string, name: string,
sticker_ids: string[], sticker_ids: string[],
username: string, user_id: string,
} }
const anonymous: User = { const anonymous: User = {
name: 'Anonymous', name: 'Anonymous',
sticker_ids: [], sticker_ids: [],
username: 'anonymous', user_id: 'anonymous',
}; };
export interface State { export interface State {
@ -21,22 +20,26 @@ export interface State {
export const useSessionStore = defineStore('session', { export const useSessionStore = defineStore('session', {
actions: { actions: {
async login(username: string, password: string) { async login(username: string, password: string) {
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await fetch( const response = await fetch(
`/api/session`, `/api-2/sessions`,
{ {
method: 'POST', method: 'POST',
body: JSON.stringify({username, password}), body: formData,
}, },
); );
const json = await response.json(); const json = await response.json();
console.log('session created', json); localStorage.setItem('access_token', json.access_token);
const payload = parseJwt(json.access_token);
// Fetch the user record. // Fetch the user record.
const userStore= useUserStore(); const userStore= useUserStore();
let user = await userStore.fetch(json.username); let user = await userStore.fetch(payload.user_id);
console.log('user', user);
if (user) { if (user) {
this.user = user; this.user = user;
} }
console.log('session started');
}, },
logout() { logout() {
console.log('logout'); console.log('logout');
@ -53,7 +56,7 @@ export const useSessionStore = defineStore('session', {
return state.user.name[0] || ''; return state.user.name[0] || '';
}, },
isAnonymous: (state: State): boolean => { isAnonymous: (state: State): boolean => {
return state.user.username === 'anonymous'; return state.user.user_id === 'anonymous';
}, },
}, },
state: (): State => { state: (): State => {
@ -63,6 +66,20 @@ export const useSessionStore = defineStore('session', {
}, },
}) })
function parseJwt(token: string) {
var base64Url = token.split('.')[1];
if (!base64Url) {
return undefined;
}
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useSessionStore, import.meta.hot)) import.meta.hot.accept(acceptHMRUpdate(useSessionStore, import.meta.hot))
} }

View file

@ -7,14 +7,15 @@ import { useUserStore } from '@/stores/userStore'
export interface Sticker { export interface Sticker {
color: string, color: string,
createdBy: string | null, createdBy: string | null,
id: string,
layout: string, layout: string,
orgs: string[],
message1: string | null, message1: string | null,
message2: string | null, message2: string | null,
message3: string | null, message3: string | null,
message4: string | null, message4: string | null,
owner: string | null, owner: string | null,
id: string, orgs: string[],
url: string | null,
} }
export const activist: Sticker = { export const activist: Sticker = {
@ -46,8 +47,8 @@ export interface State {
stickers: Sticker[]; stickers: Sticker[];
} }
function randomColor() { function randomColor(): string {
return colors[Math.floor(Math.random() * colors.length)]; return colors[Math.floor(Math.random() * colors.length)] as string;
} }
export const useStickersStore = defineStore('stickers', { export const useStickersStore = defineStore('stickers', {
@ -65,8 +66,14 @@ export const useStickersStore = defineStore('stickers', {
return; return;
} }
// TODO: Check if we have it already and if it's stale. // TODO: Check if we have it already and if it's stale.
const accessToken = localStorage.getItem('access_token');
const response = await fetch( const response = await fetch(
`/api/sticker/${id}`, `/api-2/sticker/${id}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
); );
const json = await response.json(); const json = await response.json();
this.stickers.push(json); this.stickers.push(json);
@ -77,9 +84,12 @@ export const useStickersStore = defineStore('stickers', {
return; return;
} }
const response = await fetch( const response = await fetch(
`/api/sticker/${id}`, `/api-2/sticker/${id}`,
{ {
body: JSON.stringify(sticker), body: JSON.stringify(sticker),
headers: {
'Content-Type': 'application/json',
},
method: 'PUT', method: 'PUT',
}, },
); );
@ -88,7 +98,7 @@ export const useStickersStore = defineStore('stickers', {
// this.sticker = json; // this.sticker = json;
}, },
setOwner(sticker: Sticker, user: User) { setOwner(sticker: Sticker, user: User) {
sticker.owner = user.username; sticker.owner = user.user_id;
this.put(sticker.id); this.put(sticker.id);
const userStore= useUserStore(); const userStore= useUserStore();
userStore.addSticker(user.username, sticker.id); userStore.addSticker(user.username, sticker.id);

View file

@ -1,5 +1,5 @@
export interface User { export interface User {
name: string, name: string,
sticker_ids: string[], sticker_ids: string[],
username: string, user_id: string,
} }

View file

@ -10,8 +10,8 @@ export interface State {
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
actions: { actions: {
addSticker(username: string, id: string) { addSticker(user_id: string, id: string) {
let user = this.getUser(username); let user = this.getUser(user_id);
if (user) { if (user) {
user.sticker_ids.push(id); user.sticker_ids.push(id);
} }
@ -19,20 +19,28 @@ export const useUserStore = defineStore('user', {
addUser(user: User) { addUser(user: User) {
this.users.push(user); this.users.push(user);
}, },
async fetch(username: string) { async fetch(user_id: string) {
let user = this.getUser(username); let user = this.getUser(user_id);
if (!user) { if (!user) {
// TODO: Check if we have it already and if it's stale. // TODO: Check if we have it already and if it's stale.
const response = await fetch( const response = await fetch(
`/api/user/${username}`, `/api-2/user/${user_id}`,
); );
user = await response.json(); if (!response.ok) {
if (user) { throw new Error(`HTTP error! status: ${response.status}`);
this.users.push(user); }
const user_data = await response.json();
if (user_data !== undefined) {
user = {
user_id: user_data.id,
name: user_data.display_name,
sticker_ids: [],
};
this.addUser(user);
// prepop sticker store with user stickers // pre-pop sticker store with user stickers
const stickersStore = useStickersStore(); const stickersStore = useStickersStore();
user.sticker_ids.forEach((sticker_id: string) => { user.sticker_ids?.forEach((sticker_id: string) => {
stickersStore.fetch(sticker_id); stickersStore.fetch(sticker_id);
}) })
} }
@ -42,8 +50,8 @@ export const useUserStore = defineStore('user', {
}, },
getters: { getters: {
getUser: (state: State) => { getUser: (state: State) => {
return (username: string): User | undefined => { return (user_id: string): User | undefined => {
return state.users.find((user: User) => user.username === username) return state.users.find((user: User) => user.user_id === user_id)
}; };
}, },
}, },

View file

@ -8,15 +8,15 @@ import Button from "primevue/button";
<Panel header="user1 contributed to project1"> <Panel header="user1 contributed to project1">
<Button <Button
as="router-link" as="router-link"
to="/profile/joe"> to="/profile/b356ab50-4657-4b60-aba9-71a2baab3bfe">
Joe's Profile Joe's Profile
</Button> </Button>
</Panel> </Panel>
<Panel header="user2 contributed to project2"> <Panel header="user2 contributed to project2">
<Button <Button
as="router-link" as="router-link"
to="/profile/jen"> to="/profile/2f0ad329-4f24-4d22-8827-d1a5d3237a4d">
Jen's Profile Kim's Profile
</Button> </Button>
</Panel> </Panel>
<Panel header="user3 contributed to project3"> <Panel header="user3 contributed to project3">

View file

@ -19,14 +19,18 @@ const route = useRoute();
const next = route.query.next; const next = route.query.next;
function handleLogin({ valid }) { function handleLogin({ valid } : { valid: boolean }) {
if (!valid) { if (!valid) {
console.log('Input is not valid.') console.log('Input is not valid.')
return; return;
} }
sessionStore.login(username.value, password.value).then(() => { sessionStore.login(username.value, password.value).then(() => {
if (typeof route.query.next !== 'string') {
console.error('next is not valid string.')
return;
}
console.log(`go to ${next}`) console.log(`go to ${next}`)
router.push(next); router.push(route.query.next);
}).catch((error) => { }).catch((error) => {
console.log(error); console.log(error);
}); });
@ -36,8 +40,17 @@ const initialValues = reactive({
username: 'joe', username: 'joe',
password: 'password', password: 'password',
}); });
const resolver = ({ values }) => { interface MessageType {
const errors = {}; message: string;
}
// TODO: Get the proper type. Maybe Record<string, any> in primevue/
// forms/forms.
interface FormErrors {
username?: MessageType[];
password?: MessageType[];
}
const resolver = ({ values }: { values: Record<string, any> }) => {
const errors: FormErrors = {};
if (!values.username) { if (!values.username) {
errors.username = [{message: 'Username is required.'}]; errors.username = [{message: 'Username is required.'}];
} }
@ -46,9 +59,8 @@ const resolver = ({ values }) => {
errors.password = [{message: 'Password is required.'}]; errors.password = [{message: 'Password is required.'}];
} }
password.value = values.password; password.value = values.password;
return { errors } return { values, errors }
}; };
</script> </script>
<template> <template>

68
src/views/PrintView.vue Normal file
View file

@ -0,0 +1,68 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import Avatar from 'primevue/avatar'
import Panel from 'primevue/panel'
import Sticker from '@/components/Sticker.vue'
import { type User } from '@/stores/sessionStore'
import { useUserStore } from '@/stores/userStore'
import { useStickersStore } from '@/stores/stickersStore'
const userStore = useUserStore();
const { getUser } = userStore;
const route = useRoute();
const username = ref<string>(route.params.username as string);
console.log(username);
let user = ref<User>();
const stickersStore = useStickersStore();
onMounted(async () => {
await userStore.fetch(username.value);
user.value = getUser(username.value);
});
</script>
<template>
<!-- <Panel>-->
<!-- from {{ user?.name }}-->
<!-- </Panel>-->
<Panel>
<div class="stickers">
<div class="stickerWrapper" v-for="sticker_id in user?.sticker_ids">
<RouterLink :to="{ name: 'sticker', params: { id: sticker_id }}">
<Sticker v-bind="stickersStore.getSticker(sticker_id)" />
</RouterLink>
</div>
</div>
<div class="stickers">
<div v-for="sticker in stickersStore.getStickersCreatedBy(user?.username)">
<div class="stickerWrapper">
<Sticker v-bind="sticker" />
</div>
</div>
</div>
</Panel>
</template>
<style scoped>
.stickers {
display: flex;
flex-direction: column;
gap: 10px;
}
.stickerWrapper {
background-color: #F2F2F2;
display: flex;
flex-direction: column;
padding: 10px;
.sticker {
width: 300px;
@media (min-width: 640px) {
zoom: 250%;
}
}
}
</style>

View file

@ -21,17 +21,17 @@ const sessionStore = useSessionStore();
const userStore = useUserStore(); const userStore = useUserStore();
const { getUser } = userStore; const { getUser } = userStore;
const route = useRoute(); const route = useRoute();
const username = ref<string>(route.params.username as string); const user_id = ref<string>(route.params.user_id as string);
let user = ref<User>(); let user = ref<User>();
const stickersStore = useStickersStore(); const stickersStore = useStickersStore();
onMounted(async () => { onMounted(async () => {
await userStore.fetch(username.value); await userStore.fetch(user_id.value);
user.value = getUser(username.value); user.value = getUser(user_id.value);
}); });
const myPage = computed(() => { const myPage = computed(() => {
return !sessionStore.isAnonymous && sessionStore.user.username == user.value?.username; return !sessionStore.isAnonymous && sessionStore.user.user_id == user.value?.user_id;
}); });
const meterValue = ref([ const meterValue = ref([
@ -88,8 +88,13 @@ const events = [{
v-if="myPage"> v-if="myPage">
New sticker New sticker
</Button> </Button>
<Button
as="router-link"
:to="{ name: 'print', params: { user_id: user?.user_id }}">
Print
</Button>
<div class="stickers"> <div class="stickers">
<div v-for="sticker in stickersStore.getStickersCreatedBy(user?.username)"> <div v-for="sticker in stickersStore.getStickersCreatedBy(user?.user_id)">
<div class="stickerWrapper"> <div class="stickerWrapper">
<RouterLink :to="{ name: 'sticker', params: { id: sticker.id }}"> <RouterLink :to="{ name: 'sticker', params: { id: sticker.id }}">
<Sticker v-bind="sticker" /> <Sticker v-bind="sticker" />
@ -115,7 +120,7 @@ const events = [{
:pt="{ eventOpposite: { class: 'hidden' } }" :pt="{ eventOpposite: { class: 'hidden' } }"
:value="events"> :value="events">
<template #marker="slotProps"> <template #marker="slotProps">
<span class="flex w-8 h-8 items-center justify-center text-white rounded-full z-10 shadow-sm" :style="{ backgroundColor: slotProps.item.color }"> <span class="flex w-8 h-8 items-center justify-center text-white rounded-full z-10 shadow-xs" :style="{ backgroundColor: slotProps.item.color }">
<i :class="slotProps.item.icon"></i> <i :class="slotProps.item.icon"></i>
</span> </span>
</template> </template>
@ -155,6 +160,8 @@ const events = [{
</template> </template>
<style scoped> <style scoped>
@reference "tailwindcss";
.profilePage { .profilePage {
@apply p-2 gap-2; @apply p-2 gap-2;
display: flex; display: flex;
@ -172,7 +179,7 @@ const events = [{
padding: 10px; padding: 10px;
.sticker { .sticker {
width: 300px; width: 300px;
@media (screen(sm)) { @media (min-width: 640px) {
zoom: 250%; zoom: 250%;
} }
} }

View file

@ -75,7 +75,7 @@ const orgOptions = computed(() => {
<div id="StickerPage"> <div id="StickerPage">
<!-- <Button class="w-20" type="submit" severity="secondary" label="Take a snapshot" />--> <!-- <Button class="w-20" type="submit" severity="secondary" label="Take a snapshot" />-->
<div id="StickerWrapper" class="sticky z-10"> <div id="StickerWrapper" class="sticky z-10">
<Sticker v-bind="sticker" id="Sticker1" /> <Sticker v-bind="sticker" class="medium" />
<!-- <p>8-inch sticker</p>--> <!-- <p>8-inch sticker</p>-->
<!-- <Sticker id="Sticker2" v-bind="sticker2" />--> <!-- <Sticker id="Sticker2" v-bind="sticker2" />-->
</div> </div>
@ -206,6 +206,9 @@ const orgOptions = computed(() => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@reference "tailwindcss";
@reference "tailwindcss-primeui";
label { label {
@apply font-medium text-surface-900 dark:text-surface-0 mb-1 block; @apply font-medium text-surface-900 dark:text-surface-0 mb-1 block;
} }
@ -226,9 +229,9 @@ label {
gap: 1em; gap: 1em;
padding: 20px; padding: 20px;
top: 60px; top: 60px;
#Sticker1 { .medium {
width: 300px; width: 300px;
@media (screen(sm)) { @media (min-width: 640px) {
zoom: 250%; zoom: 250%;
} }
} }

View file

@ -37,6 +37,10 @@ const second = computed(() => {
}) })
function claimSticker() { function claimSticker() {
if (!sticker.value) {
console.error("No sticker to set owner");
return;
}
stickersStore.setOwner(sticker.value, sessionStore.user as User); stickersStore.setOwner(sticker.value, sessionStore.user as User);
} }
@ -93,7 +97,7 @@ function claimSticker() {
padding: 10px; padding: 10px;
#Sticker { #Sticker {
width: 300px; width: 300px;
@media (screen(sm)) { @media (min-width: 640px) {
zoom: 250%; zoom: 250%;
} }
} }

View file

@ -1,8 +1,11 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
import primeui from 'tailwindcss-primeui'
export default { export default {
content: [ content: [
"./index.html", "./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}", "./src/**/*.{vue,js,ts,jsx,tsx}",
"./node_modules/**/*.{vue,js,ts,jsx,tsx}",
], ],
theme: { theme: {
extend: { extend: {
@ -13,5 +16,5 @@ export default {
}, },
}, },
}, },
plugins: [require('tailwindcss-primeui')], plugins: [primeui],
} }

View file

@ -1,18 +1,20 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' // import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
// vueDevTools(), // vueDevTools(),
tailwindcss(),
], ],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
server: { server: {
@ -22,6 +24,12 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''),
},
'^/api-2/': {
target: 'http://127.0.0.1:5000',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api-2/, ''),
} }
}, },
}, },