Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(story): add table design #17

Merged
merged 1 commit into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions story-starter/starters/nuxt/components/Pagination.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
defineProps<{
goToPage: (page: number) => void;
currentPage: number;
hasPreviousPage: boolean;
hasNextPage: boolean;
numberOfPages: number;
}>();
</script>

<template>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the big diff. It was inevitable as I split stuff into smaller chunks. However, the majority of the code is the same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love to see some code splitting <3

<div class="join">
<button
class="btn btn-ghost btn-sm join-item"
@click="goToPage(currentPage - 1)"
:disabled="!hasPreviousPage"
>
<span class="sr-only">Previous Page</span>
<LucideChevronLeft :size="16" />
</button>
<div v-for="(_item, idx) in new Array(numberOfPages)">
<button
class="font-normal btn btn-ghost btn-sm join-item"
:class="{
'current-page': currentPage === idx + 1,
}"
:disabled="currentPage === idx + 1"
@click="goToPage(idx + 1)"
>
{{ idx + 1 }}
</button>
</div>
<button
class="btn btn-ghost btn-sm join-item"
@click="goToPage(currentPage + 1)"
:disabled="!hasNextPage"
>
<span class="sr-only">Next Page</span>
<LucideChevronRight :size="16" />
</button>
</div>
</template>

<style scoped>
.current-page {
@apply text-primary opacity-50;
}
</style>
1 change: 1 addition & 0 deletions story-starter/starters/nuxt/components/StoryActionBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<template></template>
78 changes: 26 additions & 52 deletions story-starter/starters/nuxt/components/StoryList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ const unselectAll = () => {
const allStoryIdsPerPage = data.value?.stories.map((story) => story.id) || [];
unselectStories(allStoryIdsPerPage);
};
const onChange = (event: any, id: number) => {
if (event.target.checked) {

const updateStorySelection = (id: number, checked: boolean) => {
if (checked) {
selectStories(id);
return;
} else {
unselectStories(id);
}

unselectStories(id);
};
</script>

Expand All @@ -58,53 +58,27 @@ const onChange = (event: any, id: number) => {
<LucideLoader2 class="text-primary animate-spin" />
</div>
<div v-if="data">
<p>Number of selected stories {{ selectedStories.length }}</p>
<button class="btn" @click="selectAll">Select All</button>
<button class="btn" @click="unselectAll">Unselect All</button>
<div v-for="(story, index) in data.stories" :key="index">
<div class="form-control">
<label class="justify-start gap-2 cursor-pointer label">
<input
class="checkbox checkbox-xs"
type="checkbox"
:id="story.id.toString()"
:name="story.id.toString()"
@change="(e) => onChange(e, story.id)"
:checked="isStorySelected(story.id)"
/>
<label class="label-text" :for="story.id.toString()"
>{{ story.name }} (/{{ story.slug }})</label
>
</label>
</div>
</div>

<p>Current Page {{ currentPage }}</p>

<div class="join">
<button
class="btn btn-ghost btn-sm join-item"
@click="goToPage(currentPage - 1)"
:disabled="!hasPreviousPage"
>
Previous Page
</button>
<div v-for="(_item, idx) in new Array(numberOfPages)">
<button
class="btn btn-ghost btn-sm join-item"
:disabled="currentPage === idx + 1"
@click="goToPage(idx + 1)"
>
{{ idx + 1 }}
</button>
</div>
<button
class="btn btn-ghost btn-sm join-item"
@click="goToPage(currentPage + 1)"
:disabled="!hasNextPage"
>
Next Page
</button>
<table class="w-full table-fixed">
<StoryListHeader />
<tbody>
<StoryListItem
v-for="(story, index) in data.stories"
:key="index"
:story="story"
:checked="isStorySelected(story.id)"
@change="({ id, checked }) => updateStorySelection(id, checked)"
class="even:bg-gray-50"
/>
</tbody>
</table>
<div class="flex justify-center mt-8">
<Pagination
:goToPage="goToPage"
:currentPage="currentPage"
:hasPreviousPage="hasPreviousPage"
:hasNextPage="hasNextPage"
:numberOfPages="numberOfPages"
/>
</div>
</div>
</template>
30 changes: 30 additions & 0 deletions story-starter/starters/nuxt/components/StoryListHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<thead class="bg-gray-100">
<tr>
<th class="flex items-center gap-3">
<label class="label">
<input
class="checkbox checkbox-xs"
type="checkbox"
id="story-select-all"
@change=""
:checked="false"
/>
<label class="sr-only label-text" for="story-select-all"
>Toggle All Stories</label
>
</label>
<span>Name</span>
</th>
<th class="w-28">Content Type</th>
<th class="w-28">Last Update</th>
<th class="w-28">Author</th>
</tr>
</thead>
</template>

<style scoped>
th {
@apply font-normal text-sm text-gray-600 text-left p-2;
}
</style>
71 changes: 71 additions & 0 deletions story-starter/starters/nuxt/components/StoryListItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { Story } from '~/types/story';

const props = defineProps<{
story: Story;
checked: boolean;
}>();

const emit = defineEmits<{
change: [
value: {
id: number;
checked: boolean;
}
];
}>();

const lastUpdate = computed(() => {
const timestamp = props.story.updated_at || props.story.created_at;
const [date, time] = timestamp.split('T');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to find out how nuxt handles the formatting of dates and https://nuxt.com/modules/date-fns there is this, but for now a new dependency might be too much so its fine to leave it as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah, date-fns is definitely a great choice for date formatting as soon as we need to really format it :)

return [date, time.slice(0, 5)];
});

const onChange = (event: Event) => {
if (!event.target) {
return;
}
emit('change', {
id: props.story.id,
checked: (event.target as HTMLInputElement).checked,
});
};
</script>

<template>
<tr class="hover:bg-gray-100">
<td>
<label class="justify-start gap-4 cursor-pointer label">
<input
class="checkbox checkbox-xs"
type="checkbox"
:id="story.id.toString()"
:name="story.id.toString()"
@change="onChange"
:checked="props.checked"
/>
<label class="label-text" :for="story.id.toString()"
>{{ story.name }}<br />
<span class="text-xs font-light text-gray-400"
>/{{ story.slug }}</span
></label
>
</label>
</td>
<td class="text-sm font-light text-gray-500">
{{ story.content_type }}
</td>
<td class="text-xs font-light text-gray-500">
{{ lastUpdate[0] }}<br />{{ lastUpdate[1] }}
</td>
<td class="text-sm font-light text-gray-500">
{{ story.last_author.friendly_name }}
</td>
</tr>
</template>

<style scoped>
td {
@apply p-2;
}
</style>
19 changes: 9 additions & 10 deletions story-starter/starters/nuxt/composables/useStories.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { Ref, UnwrapRef } from 'vue';
import type { Stories } from '~/types/story';
import { type ISbStoryData } from 'storyblok-js-client';
import type { StoriesResponse, Story } from '~/types/story';

type UseStories = (props?: { perPage?: number }) => Promise<{
data: Ref<Stories | undefined>;
data: Ref<StoriesResponse | undefined>;
hasNextPage: Ref<UnwrapRef<boolean>>;
hasPreviousPage: Ref<UnwrapRef<boolean>>;
isLoading: Ref<UnwrapRef<boolean>>;
Expand All @@ -12,25 +11,25 @@ type UseStories = (props?: { perPage?: number }) => Promise<{
selectStories: (id: number | number[]) => void;
isStorySelected: (id: number) => boolean;
unselectStories: (id: number | number[]) => void;
selectedStories: Ref<ISbStoryData[]>;
selectedStories: Ref<Story[]>;
goToPage: (page: number) => void;
error: Ref<Error | null>;
}>;

export const useStories: UseStories = async (props) => {
const selectedStoryIds = useState<Set<number>>(() => new Set<number>());
const selectedStories = useState<Map<number, ISbStoryData>>(() => new Map());
const selectedStories = useState<Map<number, Story>>(() => new Map());
const currentPage = useState(() => 1);

const { data, pending, error } = await useFetch<Stories, Error>(
const { data, pending, error } = await useFetch<StoriesResponse, Error>(
'/api/stories',
{
server: false,
query: {
perPage: props?.perPage || 25,
page: currentPage,
},
},
}
);

const selectedStoriesInArray = computed(() => [
Expand All @@ -45,7 +44,7 @@ export const useStories: UseStories = async (props) => {
});

const nextPage = computed(() =>
getNextPage(toValue(numberOfPages), currentPage.value),
getNextPage(toValue(numberOfPages), currentPage.value)
);
const previousPage = computed(() => getPreviousPage(currentPage.value));
const hasPreviousPage = computed(() => Boolean(previousPage.value));
Expand All @@ -71,7 +70,7 @@ export const useStories: UseStories = async (props) => {
const selectedStory = data.value.stories.find((story) => story.id === i);
if (selectedStory) {
selectedStoryIds.value.add(i);
selectedStories.value.set(i, selectedStory as ISbStoryData);
selectedStories.value.set(i, selectedStory);
}
}
};
Expand All @@ -86,7 +85,7 @@ export const useStories: UseStories = async (props) => {
};

return {
data: data as Ref<Stories>,
data: data as Ref<StoriesResponse>,
hasPreviousPage,
hasNextPage,
isLoading: pending,
Expand Down
8 changes: 4 additions & 4 deletions story-starter/starters/nuxt/server/api/stories.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import StoryblokClient, { type ISbStoryData } from 'storyblok-js-client';
import { object, coerce, optional, number } from 'valibot';
import { parseQuery } from '../utils/parse';
import { Stories } from '~/types/story';
import { StoriesResponse } from '~/types/story';

type Version = 'published' | 'draft';

Expand All @@ -10,9 +10,9 @@ type GetStories = (props: {
perPage?: number;
page?: number;
version?: Version;
}) => Promise<Stories>;
}) => Promise<StoriesResponse>;

export default defineEventHandler(async (event): Promise<Stories> => {
export default defineEventHandler(async (event): Promise<StoriesResponse> => {
const { spaceId, accessToken } = event.context.appSession;
// We're using `coerce(number(), Number)` instead of `number()`.
// When requesting with query parameters like:
Expand Down Expand Up @@ -60,7 +60,7 @@ const storyblokFetch = (accessToken: string) => {
version: version ?? (defaults.version as Version),
per_page: perPage ?? defaults.perPage,
page: page ?? defaults.page,
},
}
);

return {
Expand Down
16 changes: 14 additions & 2 deletions story-starter/starters/nuxt/types/story.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { type ISbStoryData } from 'storyblok-js-client';

export type Stories = {
stories: ISbStoryData[];
export type StoriesResponse = {
stories: Story[];
perPage: number;
total: number;
};

export type Story = ISbStoryData & {
// these are missing from `ISbStoryData`
content_type: string;
updated_at?: string;
last_author: {
id: number;
userid: string;
friendly_name: string;
avatar: string;
};
};