Skip to content

Commit

Permalink
It's alive!
Browse files Browse the repository at this point in the history
  • Loading branch information
NoelDeMartin committed Aug 10, 2024
1 parent 2239ee6 commit 06932ea
Show file tree
Hide file tree
Showing 53 changed files with 1,138 additions and 116 deletions.
1 change: 1 addition & 0 deletions cypress/e2e/all.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See https://github.com/cypress-io/cypress/discussions/21628

// If you want to use it, just uncomment these lines:
// import './animations.cy';
// import './cloud.cy';
// import './navigation.cy';
// import './onboarding.cy';
Expand Down
64 changes: 64 additions & 0 deletions cypress/e2e/animations.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
describe.skip('Animations', () => {
beforeEach(() => {
cy.visit('/');
cy.createStubs();
});

it('Tasks collapse/expand (long)', () => {
cy.visit('/household/groceries');

cy.press('Completed');
cy.wait(1000);

cy.press('Completed');
cy.wait(1000);

cy.contains('li', 'Bananas').within(() => cy.get('input[type="checkbox"]').click());
cy.contains('li', 'Orange juice').within(() => cy.get('input[type="checkbox"]').click());
cy.wait(1000);

cy.press('Completed');
cy.wait(1000);

cy.press('Completed');
cy.wait(1000);
});

it('Tasks collapse/expand (short)', () => {
cy.visit('/household');

cy.press('Completed');
cy.wait(1000);

cy.press('Completed');
cy.wait(1000);

cy.contains('li', 'Clean room').within(() => cy.get('input[type="checkbox"]').click());
cy.wait(1000);

cy.press('Completed');
cy.wait(1000);

cy.press('Completed');
cy.wait(1000);
});

it('Tasks toggle', () => {
cy.visit('/household/groceries');

cy.press('Completed');
cy.wait(1000);

cy.contains('li', 'Bananas').within(() => cy.get('input[type="checkbox"]').click());
cy.wait(1000);

cy.contains('li', 'Orange juice').within(() => cy.get('input[type="checkbox"]').click());
cy.wait(1000);

cy.press('Completed');
cy.wait(1000);

cy.contains('li', 'Orange juice').within(() => cy.get('input[type="checkbox"]').click());
cy.wait(1000);
});
});
2 changes: 1 addition & 1 deletion cypress/e2e/cloud.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ describe('Cloud', () => {
cy.get('@createMangaContainer.all').should('have.length', 1);
cy.get('@createMainTask.all').should('have.length', 4);
cy.get('@createHouseholdTask.all').should('have.length', 2);
cy.get('@createGroceriesTask.all').should('have.length', 3);
cy.get('@createGroceriesTask.all').should('have.length', 30);
cy.get('@createRecipesTask.all').should('have.length', 3);
cy.get('@createJapaneseTask.all').should('have.length', 3);
cy.get('@createMangaTask.all').should('have.length', 2);
Expand Down
5 changes: 2 additions & 3 deletions cypress/e2e/navigation.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ describe('Navigation', () => {
cy.press('Groceries');
cy.seeActiveWorkspace('Household');
cy.seeActiveList('Groceries');
cy.see('Nuts');
cy.see('Chickpeas');
cy.see('Tomatoes');
cy.see('Bananas');
cy.see('Orange juice');

cy.press('Recipes');
cy.seeActiveWorkspace('Household');
Expand Down
35 changes: 32 additions & 3 deletions cypress/support/commands/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,38 @@ export function createStubs(open: boolean = true): void {
await household.relatedTasks.create({ name: 'Clean room' });
await household.relatedTasks.create({ name: 'Clean Neko\'s litter box' }).then((task) => task.toggle());

await groceries.relatedTasks.create({ name: 'Chickpeas' });
await groceries.relatedTasks.create({ name: 'Tomatoes' });
await groceries.relatedTasks.create({ name: 'Nuts' });
await groceries.relatedTasks.create({ name: 'Bananas' });
await groceries.relatedTasks.create({ name: 'Orange juice' });
await groceries.relatedTasks.create({ name: 'Whole grain bread' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Fresh apples and pears' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Baby carrots' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Red and green grapes' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Peanut butter' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Cucumber' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Fresh strawberries' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Almond milk' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Romaine lettuce' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Mixed berries (frozen)' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Broccoli florets' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Whole grain pasta' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Fresh tomatoes' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Rolled oats' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Bell peppers (variety pack)' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Fresh basil leaves' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Canned black beans' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Brown rice' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Fresh oranges and lemons' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Canned tomatoes' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Baby spinach' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Mixed nuts' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Fresh garlic and onions' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Avocado' }).then((task) => task.toggle());
await groceries.relatedTasks
.create({ name: 'Fresh blueberries and raspberries' })
.then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Zucchini and squash' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Whole wheat tortillas' }).then((task) => task.toggle());
await groceries.relatedTasks.create({ name: 'Applesauce (unsweetened)' }).then((task) => task.toggle());

await recipes.relatedTasks.create({ name: 'Ramen' });
await recipes.relatedTasks.create({ name: 'Pizza' }).then((task) => task.toggle());
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"test:serve-pod": "community-solid-server -l warn"
},
"dependencies": {
"@aerogel/core": "0.0.0-next.2d501d5a5dfe5863218d65b11ac78ea023da9127",
"@aerogel/core": "0.0.0-next.6c539d8e63b397d4bb6c3d61a7f20a4d108b1cdd",
"@aerogel/plugin-i18n": "0.0.0-next.ee913f442ea0f8f8c64301001984e83797c6b7e3",
"@aerogel/plugin-offline-first": "0.0.0-next.6c02970f90d4e979a72530f1fa0c650785d6538f",
"@aerogel/plugin-routing": "0.0.0-next.6c02970f90d4e979a72530f1fa0c650785d6538f",
Expand All @@ -31,7 +31,7 @@
"@headlessui/vue": "^1.7.22",
"@intlify/unplugin-vue-i18n": "^0.12.2",
"@noeldemartin/solid-utils": "0.4.0-next.30aceafb9d58f505d02a146d8e81f2e3a041b92f",
"@noeldemartin/utils": "0.5.1-next.369ec16c5fa546a6a2e7512501add6b0e5b1a3ac",
"@noeldemartin/utils": "0.5.1-next.82707467527729a9fe7c2155959beb4b5ea30b81",
"@popperjs/core": "^2.11.8",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
Expand Down
10 changes: 8 additions & 2 deletions src/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
<div class="flex-1">
<div :style="`width: ${$focus.footerLeftPadding ?? 0}px`" />
</div>
<footer class="flex w-full max-w-screen-xl items-center justify-center gap-1 p-3 text-sm text-gray-500">
<AnimatedElement
as="footer"
class="flex w-full max-w-screen-xl items-center justify-center gap-1 p-3 text-sm text-gray-500"
:group="$focus.footerAnimation?.group"
:animation="$focus.footerAnimation?.animation"
@animate="$focus.footerAnimation?.animate"
>
<TextLink @click="$ui.openModal(AboutModal)">
{{ $t('footer.about') }}
</TextLink>
Expand All @@ -15,7 +21,7 @@
<TextLink :url="$app.versionUrl">
{{ $app.versionName }}
</TextLink>
</footer>
</AnimatedElement>
<div class="flex-1">
<div :style="`width: ${$focus.footerRightPadding ?? 0}px`" />
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import offlineFirst from '@aerogel/plugin-offline-first';
import routing from '@aerogel/plugin-routing';
import solid from '@aerogel/plugin-solid';
import soukai from '@aerogel/plugin-soukai';
import vivant from '@/vivant/aerogel';
import { bootstrap } from '@aerogel/core';

import './assets/css/styles.css';
Expand All @@ -20,6 +21,7 @@ bootstrap(App, {
routing({ routes, bindings }),
solid(),
offlineFirst(),
vivant(),
],
install(app) {
Object.assign(app.config.globalProperties, globals);
Expand Down
118 changes: 81 additions & 37 deletions src/pages/workspace/WorkspaceContentBody.vue
Original file line number Diff line number Diff line change
@@ -1,61 +1,89 @@
<template>
<div class="flex flex-col px-4">
<AnimatedGroup ref="$group" class="flex flex-col px-4" :duration="allPendingCompleted ? 500 : 300">
<TasksForm v-if="$tasksList.tasks?.length" ref="$tasksForm" @submit="createTask($event)" />
<TasksList
v-if="tasks.pending.length"
:tasks="tasks.pending"
:disable-editing="disableEditing"
class="mt-4"
/>
<TasksStart v-else-if="!tasks.completed.length" @create="createTask($event)" />
<TasksEmpty v-else-if="!$focus.showCompleted" />
<div v-if="tasks.completed.length" class="mt-4">
<TextButton
color="clear"
class="ml-1 pl-1 pr-2 font-medium uppercase tracking-wider"
:aria-label="$focus.showCompleted ? $t('tasks.hideCompleted') : $t('tasks.showCompleted')"
@click="$focus.toggleCompleted()"

<div class="relative flex flex-grow flex-col">
<TasksList
class="transition-[margin] duration-500"
:tasks="tasks.pending"
:disable-editing="disableEditing"
:class="tasks.pending.length ? 'mt-4' : 'mt-0'"
/>

<TasksStart v-if="!tasks.pending.length && !tasks.completed.length" @create="createTask($event)" />

<AnimatedTransition
enter-active-class="transition-[opacity,transform] delay-200 duration-500"
enter-from-class="max-h-0 !py-0 opacity-0 -translate-y-12"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-[opacity,transform] duration-500"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="absolute top-0 opacity-0 -translate-y-52"
leave-animation="freeze"
>
<i-zondicons-cheveron-right
class="h-6 w-6 transition-transform"
:class="{ 'rotate-90': $focus.showCompleted }"
/>
<span>{{ $t('tasks.completed') }}</span>
</TextButton>
<Transition
enter-from-class="scale-y-0 opacity-0"
enter-active-class="origin-top-left transition-[transform,opacity] duration-[500ms]"
enter-to-class="scale-y-100 opacity-100"
leave-from-class="scale-y-100 opacity-100"
leave-active-class="origin-top-left transition-[transform,opacity] duration-[500ms]"
leave-to-class="scale-y-0 opacity-0"
<TasksEmpty v-if="allPendingCompleted && !$focus.showCompleted" />
</AnimatedTransition>

<div
v-if="tasks.completed.length"
class="flex flex-col"
:class="{
'mt-4 flex-grow': !allPendingCompleted,
'has-[.completed-tasks-wrapper:not(.absolute)]:mt-4': allPendingCompleted,
'has-[.completed-tasks-wrapper:not(.absolute)]:flex-grow': allPendingCompleted,
}"
>
<TasksList
v-if="$focus.showCompleted"
:tasks="tasks.completed"
:disable-editing="disableEditing"
class="mt-4"
/>
</Transition>
<TextButton
v-animate-layout
color="clear"
class="ml-1 self-start pl-1 pr-2 font-medium uppercase tracking-wider"
:aria-label="$focus.showCompleted ? $t('tasks.hideCompleted') : $t('tasks.showCompleted')"
@click="$focus.toggleCompleted()"
>
<i-zondicons-cheveron-right
class="h-6 w-6 transition-transform"
:class="{ 'rotate-90': $focus.showCompleted }"
/>
<span>{{ $t('tasks.completed') }}</span>
</TextButton>
<AnimatedTransition
:enter-from-class="`${allPendingCompleted ? 'absolute bottom-0' : ''} h-0`"
:leave-to-class="`${allPendingCompleted ? 'absolute bottom-0' : ''} h-0`"
@enter="allPendingCompleted ? toggleCompletedTasks($event) : slideDown($event.firstElementChild)"
@leave="
allPendingCompleted
? toggleCompletedTasks($event)
: ($event.classList.remove('h-0'), slideUp($event.firstElementChild))
"
>
<div v-if="$focus.showCompleted" class="completed-tasks-wrapper overflow-hidden">
<TasksList :tasks="tasks.completed" :disable-editing="disableEditing" class="mt-4" />
</div>
</AnimatedTransition>
</div>
</div>
</div>
</AnimatedGroup>
</template>

<script setup lang="ts">
import { arrayGroupBy, arraySorted, compare } from '@noeldemartin/utils';
import { Cloud } from '@aerogel/plugin-offline-first';
import { computed, ref } from 'vue';
import { computed, onUnmounted, ref, watch, watchEffect } from 'vue';
import { computedModels } from '@aerogel/plugin-soukai';
import { slideDown, slideUp } from '@/vivant/core';
import { UI } from '@aerogel/core';
import type { IAnimatedGroup } from '@/vivant/vue';
import Focus from '@/services/Focus';
import Task from '@/models/Task';
import TasksLists from '@/services/TasksLists';
import Workspaces from '@/services/Workspaces';
import { watchKeyboardShortcut } from '@/utils/composables';
import { toggleCompletedTasks, toggleFooter } from './animations/tasks';
import type { ITasksForm } from './components/tasks/TasksForm';
const $group = ref<IAnimatedGroup>();
const $tasksForm = ref<ITasksForm>();
const disableEditingWithKeyboard = ref(false);
const disableEditing = computed(() => UI.mobile || disableEditingWithKeyboard.value);
Expand All @@ -65,6 +93,8 @@ const tasks = computed(() => ({
pending: arraySorted(groupedTasks.value.pending ?? [], compareTasks),
completed: arraySorted(groupedTasks.value.completed ?? [], compareTasks),
}));
const showPending = computed(() => !!tasks.value.pending.length);
const allPendingCompleted = computed(() => !tasks.value.pending.length && tasks.value.completed.length);
function compareTasks(a: Task, b: Task): number {
const importantComparison = compare(b.important, a.important);
Expand Down Expand Up @@ -109,6 +139,18 @@ function changeTask(delta: 1 | -1) {
}
}
watch(
() => showPending.value,
(value) => (Focus.showCompleted &&= value),
);
watchEffect(
() =>
$group.value &&
Focus.setFooterAnimation({
group: $group.value.group,
animate: toggleFooter,
}),
);
watchKeyboardShortcut('Control', {
start: () => (disableEditingWithKeyboard.value = true),
end: () => (disableEditingWithKeyboard.value = false),
Expand All @@ -118,4 +160,6 @@ watchKeyboardShortcut('c', () => Focus.toggleCompleted());
watchKeyboardShortcut('ArrowUp', () => changeTask(-1));
watchKeyboardShortcut('ArrowDown', () => changeTask(1));
watchKeyboardShortcut('Escape', () => Workspaces.select(null));
onUnmounted(() => Focus.setFooterAnimation(null));
</script>
Loading

0 comments on commit 06932ea

Please sign in to comment.