Skip to content

Commit

Permalink
feat: Create and login to accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
aklinker1 committed Apr 30, 2024
1 parent 1a99379 commit 0adc5a5
Show file tree
Hide file tree
Showing 21 changed files with 381 additions and 42 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"dependencies": {
"@aklinker1/check": "^1.3.1",
"firebaseui": "^6.1.0",
"nanoid": "^5.0.7",
"panzoom": "^9.4.3",
"standard-version": "^9.5.0",
Expand Down
10 changes: 10 additions & 0 deletions web/components/ProfileTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts" setup>
const currentUser = useCurrentUser();
</script>

<template>
<TabListItem to="/account" hide-close>
<span v-if="!currentUser" class="text-sm">Sign In</span>
<UIcon name="i-heroicons-user-circle" class="size-6" />
</TabListItem>
</template>
47 changes: 47 additions & 0 deletions web/components/ProjectList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts" setup>
const createNewProject = useCreateNewProject();
const { data: projects, isFetching } = useProjectListQuery();
const filter = ref('');
const filteredProjects = useArrayFilter(projects, (p) =>
p.name.toLowerCase().includes(filter.value.toLowerCase()),
);
const openProject = useOpenProject();
const deleteProject = useDeleteProject();
</script>

<template>
<ul class="flex flex-col gap-2">
<li>
<UInput
v-model="filter"
placeholder="Filter projects..."
icon="i-heroicons-magnifying-glass"
/>
</li>
<ProjectListItem
v-for="project of filteredProjects"
:key="project.id"
:project
@open="openProject"
@delete="deleteProject"
/>
<li v-if="isFetching" class="flex justify-center">
<UButton
class="pointer-events-none"
loading
variant="ghost"
color="white"
disabled
>
Loading...
</UButton>
</li>
<li v-else-if="filteredProjects.length === 0" class="text-center py-16">
No projects,
<ULink variant="link" @click="createNewProject">add one</ULink>
</li>
<slot />
</ul>
</template>
2 changes: 1 addition & 1 deletion web/components/TabListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const route = useRoute();

<template>
<li
class="bg-gray-100 dark:bg-gray-900 border-r border-gray-300 dark:border-gray-800"
class="bg-gray-100 dark:bg-gray-900 border-r border-gray-300 dark:border-gray-800 shrink-0"
>
<ULink
class="px-3 flex shrink-0 h-12 justify-between items-center gap-3"
Expand Down
11 changes: 11 additions & 0 deletions web/composables/useAccountService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const firebase = createFirebaseAccountService();
const local = createLocalAccountService();

export function useAccountService() {
const user = useCurrentUser();

return computed(() => {
if (user.value == null) return local;
return firebase;
});
}
4 changes: 4 additions & 0 deletions web/composables/useAccountServiceId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function () {
const account = useAccountService();
return computed(() => account.value.id);
}
10 changes: 10 additions & 0 deletions web/composables/useCurrentUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default createSharedComposable(() => {
const user = ref(firebaseAuth.currentUser);
firebaseAuth.authStateReady().then(() => {
user.value = firebaseAuth.currentUser;
});
firebaseAuth.onAuthStateChanged(() => {
user.value = firebaseAuth.currentUser;
});
return user;
});
12 changes: 12 additions & 0 deletions web/composables/useProjectListQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/vue-query';

export default function () {
const account = useAccountService();
const accountId = useAccountServiceId();
const user = useCurrentUser();
return useQuery({
queryKey: ['projects', accountId],
queryFn: () => account.value.listProjects(),
initialData: [],
});
}
12 changes: 12 additions & 0 deletions web/composables/useSaveSettingsMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useMutation } from '@tanstack/vue-query';
import type { AccountSettings } from '~/utils';

export default function () {
const accountService = useAccountService();

return useMutation({
mutationFn(changes: Partial<AccountSettings>) {
return accountService.value.setSettings(changes);
},
});
}
29 changes: 18 additions & 11 deletions web/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,30 @@ const closeTab = useCloseTab();

<template>
<div class="fixed inset-0 flex flex-col">
<TabList class="shrink-0">
<ul class="shrink-0 flex">
<!-- Home Link -->
<TabListItem to="/" hide-close class="sticky left-0">
<UIcon name="i-heroicons-home-solid" class="w-6" />
<UIcon name="i-heroicons-home-solid" class="size-4" />
</TabListItem>

<!-- Projects -->
<TabList class="flex-1">
<!-- Projects -->
<ClientOnly>
<TabListItem
v-for="tab in tabs"
:key="tab.id"
:name="tab.name"
:to="tab.to"
@close="closeTab(tab.id)"
/>
</ClientOnly>
</TabList>

<!-- Account -->
<ClientOnly>
<TabListItem
v-for="tab in tabs"
:key="tab.id"
:name="tab.name"
:to="tab.to"
@close="closeTab(tab.id)"
/>
<ProfileTab />
</ClientOnly>
</TabList>
</ul>
<div class="flex-1 relative">
<div class="absolute inset-0 overflow-hidden">
<slot />
Expand Down
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@types/js-yaml": "^4.0.9",
"@vueuse/core": "^10.9.0",
"@vueuse/nuxt": "^10.9.0",
"firebase": "^10.11.0",
"nuxt": "^3.11.0",
"typescript": "^5.0.0",
"vue": "^3.4.21",
Expand Down
63 changes: 63 additions & 0 deletions web/pages/account.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts" setup>
import * as firebaseui from 'firebaseui';
import { EmailAuthProvider } from 'firebase/auth';
import 'firebaseui/dist/firebaseui.css';
const currentUser = useCurrentUser();
const container = ref<HTMLDivElement>();
let login: firebaseui.auth.AuthUI | undefined;
watch(container, (container) => {
if (container) {
login =
firebaseui.auth.AuthUI.getInstance() ??
new firebaseui.auth.AuthUI(firebaseAuth);
login.start(container, {
signInOptions: [EmailAuthProvider.PROVIDER_ID],
});
} else {
login?.delete();
login = undefined;
}
});
watch(currentUser, (user) => {
if (user) syncProjects();
});
async function syncProjects() {
const localService = createLocalAccountService();
const firebaseService = createFirebaseAccountService();
const localProjects = await localService.listProjects();
const firebaseProjects = await firebaseService.listProjects();
const firebaseProjectIds = new Set(firebaseProjects.map((p) => p.id));
const missingProjects = localProjects.filter(
(p) => !firebaseProjectIds.has(p.id),
);
console.log(
`Syncing ${missingProjects.length} local projects to firebase...`,
);
for (const p of missingProjects) {
await firebaseService.saveProject(p);
}
}
</script>

<template>
<div class="bg-gray-200 dark:bg-gray-800 h-full p-8 flex flex-col gap-4">
<h2 class="text-2xl font-medium line-clamp-1 truncate">Account</h2>
<template v-if="!currentUser">
<p>Login to share settings and projects between devices.</p>
<div ref="container" v-if="!currentUser" />
</template>
<template v-else>
<p>
Logged in as: {{ currentUser.displayName }} ({{ currentUser.email }})
</p>
<UButton class="self-start" @click="firebaseAuth.signOut()"
>Log out</UButton
>
</template>
</div>
</template>
31 changes: 2 additions & 29 deletions web/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@
import { version } from '~~/package.json';
const createNewProject = useCreateNewProject();
const projects = useProjects();
const filter = ref('');
const filteredProjects = useArrayFilter(projects, (p) =>
p.name.toLowerCase().includes(filter.value.toLowerCase()),
);
const openProject = useOpenProject();
const deleteProject = useDeleteProject();
</script>

<template>
Expand All @@ -28,25 +19,7 @@ const deleteProject = useDeleteProject();
>
</div>
<ClientOnly>
<ul class="flex flex-col gap-2">
<li v-if="projects.length">
<UInput
v-model="filter"
placeholder="Filter projects..."
icon="i-heroicons-magnifying-glass"
/>
</li>
<ProjectListItem
v-for="project of filteredProjects"
:key="project.id"
:project
@open="openProject"
@delete="deleteProject"
/>
<li v-if="filteredProjects.length === 0" class="text-center py-16">
No projects,
<ULink variant="link" @click="createNewProject">add one</ULink>
</li>
<ProjectList>
<li class="text-center pt-16 opacity-50">
<ULink
class="hover:underline"
Expand All @@ -69,7 +42,7 @@ const deleteProject = useDeleteProject();
>User Manual</ULink
>
</li>
</ul>
</ProjectList>
</ClientOnly>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion web/plugins/vue-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default defineNuxtPlugin((nuxt) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnMount: false,
refetchOnMount: true,
refetchOnWindowFocus: false,
retry: false,
},
Expand Down
26 changes: 26 additions & 0 deletions web/utils/accounts/AccountService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Project } from '../projects';

export interface AccountService {
id: string;
getSettings(): Promise<AccountSettings>;
setSettings(changes: Partial<AccountSettings>): Promise<void>;
listProjects(): Promise<Project[]>;
saveProject(newProject: Project): Promise<void>;
removeProject(id: string): Promise<void>;
}

export interface AccountSettings {
bladeWidth: number;
distanceUnit: string;
extraSpace: number;
optimize: 'Cuts' | 'Space';
showPartNumbers: boolean;
}

export const DEFAULT_SETTINGS: AccountSettings = {
bladeWidth: 0.125,
distanceUnit: 'in',
extraSpace: 0,
optimize: 'Cuts',
showPartNumbers: true,
};
47 changes: 47 additions & 0 deletions web/utils/accounts/FirebaseAccountService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
getDoc,
setDoc,
getDocs,
query,
deleteDoc,
collection,
doc,
} from 'firebase/firestore';
import { db, firebaseAuth, usersRef } from '../firebase';
import { DEFAULT_SETTINGS } from './AccountService';

export function createFirebaseAccountService(): AccountService {
const getUid = () => {
if (firebaseAuth.currentUser == null) throw Error('Not logged in');
return firebaseAuth.currentUser.uid;
};

const settingsDoc = () => doc(usersRef, getUid(), 'settings', 'default');
const projectsRef = () => collection(usersRef, getUid(), 'projects');
const projectDoc = (id: string) => doc(usersRef, getUid(), 'projects', id);

return {
id: 'firebase',
async getSettings() {
const res = await getDoc(settingsDoc());
return {
...DEFAULT_SETTINGS,
...(res.exists() ? res.data() : {}),
};
},
async setSettings(changes) {
await setDoc(settingsDoc(), changes, { merge: true });
},
async listProjects() {
const q = query(projectsRef());
const res = await getDocs(q);
return res.docs.map((doc) => doc.data()) as Project[];
},
async saveProject(project) {
await setDoc(projectDoc(project.id), project);
},
async removeProject(projectId) {
await deleteDoc(projectDoc(projectId));
},
};
}
Loading

0 comments on commit 0adc5a5

Please sign in to comment.