feat(fe): Tutorials page (#4120)

* Tutorials Page

* Add tutorials page

* Update Page.vue

* Changes from PR

* Updates from call

* Remove page added in error

* Update Page.vue

* Remove shallowref

* Update mixpanel name
This commit is contained in:
andrewwallacespeckle
2025-03-07 12:18:45 +00:00
committed by GitHub
parent b1ed49297b
commit 45d7d4d02b
12 changed files with 177 additions and 37 deletions
@@ -33,11 +33,12 @@
name="categories"
label="Categories"
placeholder="All categories"
class="md:min-w-80"
class="md:w-80"
allow-unset
:items="categories"
size="base"
color="foundation"
clearable
>
<template #something-selected="{ value }">
{{ isArray(value) ? value[0].name : value.name }}
@@ -56,23 +56,16 @@
<section>
<div class="flex items-center justify-between">
<h2 class="text-heading-sm text-foreground-2">Tutorials</h2>
<FormButton
color="outline"
size="sm"
to="https://www.speckle.systems/tutorials"
external
target="_blank"
>
View all
<FormButton color="outline" size="sm" :to="tutorialsRoute">
View more
</FormButton>
</div>
<div
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 pt-5"
>
<DashboardTutorialCard
v-for="tutorialItem in tutorialItems"
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-5">
<TutorialsCard
v-for="tutorialItem in tutorialItems.slice(0, 4)"
:key="tutorialItem.title"
:tutorial-item="tutorialItem"
source="dashboard"
/>
</div>
</section>
@@ -93,14 +86,15 @@ import {
docsPageUrl,
forumPageUrl,
homeRoute,
projectsRoute
projectsRoute,
tutorialsRoute
} from '~~/lib/common/helpers/route'
import type { ManagerExtension } from '~~/lib/common/utils/downloadManager'
import { downloadManager } from '~~/lib/common/utils/downloadManager'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
import type { LayoutDialogButton } from '@speckle/ui-components'
import type { PromoBanner } from '~/lib/promo-banners/types'
import { tutorials } from '~/lib/dashboard/helpers/tutorials'
import { tutorialItems } from '~/lib/dashboard/helpers/tutorials'
import { useUserProjectsUpdatedTracking } from '~~/lib/user/composables/projectUpdates'
const mixpanel = useMixpanel()
@@ -120,7 +114,6 @@ useUserProjectsUpdatedTracking()
const promoBanners = ref<PromoBanner[]>()
const openNewProject = ref(false)
const tutorialItems = shallowRef(tutorials)
const quickStartItems = shallowRef<QuickStartItem[]>([
{
title: 'Install Speckle manager',
@@ -63,6 +63,17 @@
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<NuxtLink :to="tutorialsRoute" @click="isOpenMobile = false">
<LayoutSidebarMenuGroupItem
label="Tutorials"
:active="isActive(tutorialsRoute)"
>
<template #icon>
<IconTutorials class="size-4 ml-px text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
</LayoutSidebarMenuGroup>
<LayoutSidebarMenuGroup
@@ -178,7 +189,8 @@ import {
workspaceRoute,
workspacesRoute,
workspaceCreateRoute,
connectorsRoute
connectorsRoute,
tutorialsRoute
} from '~/lib/common/helpers/route'
import { useRoute } from 'vue-router'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
@@ -0,0 +1,45 @@
<template>
<svg
width="18"
height="16"
viewBox="0 0 18 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.5 13.8154C2.64014 13.1571 3.93347 12.8105 5.25 12.8105C6.56652 12.8105 7.85986 13.1571 9 13.8154C10.1401 13.1571 11.4335 12.8105 12.75 12.8105C14.0665 12.8105 15.3599 13.1571 16.5 13.8154"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M1.5 2.98137C2.64014 2.32311 3.93347 1.97656 5.25 1.97656C6.56652 1.97656 7.85986 2.32311 9 2.98137C10.1401 2.32311 11.4335 1.97656 12.75 1.97656C14.0665 1.97656 15.3599 2.32311 16.5 2.98137"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M1.5 2.98242V13.8158"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M9 2.98242V13.8158"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16.5 2.98242V13.8158"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -6,7 +6,7 @@
<NuxtImg
:src="tutorialItem.image"
:alt="tutorialItem.title"
class="h-32 w-full object-cover"
class="aspect-video w-full object-cover"
width="400"
height="225"
/>
@@ -27,12 +27,14 @@ const mixpanel = useMixpanel()
const props = defineProps<{
tutorialItem: TutorialItem
source: 'tutorials' | 'dashboard'
}>()
const trackClick = () => {
mixpanel.track('Tutorial clicked', {
title: props.tutorialItem.title,
url: props.tutorialItem.url
url: props.tutorialItem.url,
source: props.source
})
}
</script>
@@ -0,0 +1,51 @@
<template>
<div>
<div class="flex flex-col gap-y-6">
<section class="flex items-center gap-2">
<div class="flex flex-col gap-2 flex-1">
<div class="flex items-center gap-2">
<IconTutorials class="size-4" />
<h1 class="text-heading-lg">Tutorials</h1>
</div>
<p class="text-body-sm text-foreground-2">
Get started with Speckle with step-by-step instructions for all skill
levels.
</p>
</div>
</section>
<section class="flex gap-4 flex-col">
<div class="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<TutorialsCard
v-for="tutorial in tutorialItems"
:key="tutorial.title"
:tutorial-item="tutorial"
source="tutorials"
/>
</div>
<div class="flex justify-center mt-4">
<FormButton
label="View all tutorials"
to="https://www.speckle.systems/tutorials"
target="_blank"
color="outline"
external
@click="trackViewAllClick"
>
View all tutorials
</FormButton>
</div>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { tutorialItems } from '~/lib/dashboard/helpers/tutorials'
import { useMixpanel } from '~~/lib/core/composables/mp'
const mixpanel = useMixpanel()
const trackViewAllClick = () => {
mixpanel.track('View All Tutorials Button Clicked')
}
</script>
@@ -16,6 +16,7 @@ export const verifyEmailRoute = '/verify-email'
export const verifyEmailCountdownRoute = '/verify-email?source=registration'
export const serverManagementRoute = '/server-management'
export const connectorsRoute = '/connectors'
export const tutorialsRoute = '/tutorials'
export const downloadManagerUrl = 'https://speckle.systems/download'
export const docsPageUrl = 'https://speckle.guide/'
export const forumPageUrl = 'https://speckle.community/'
@@ -1,6 +1,12 @@
import type { TutorialItem } from '~/lib/dashboard/helpers/types'
export const tutorials: TutorialItem[] = [
export const tutorialItems: TutorialItem[] = [
{
title: 'Get Civil 3D Pipe Networks Into Revit as Families',
image:
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/67b8980920e42b89aff75a3f_C3d%20to%20Revit.jpg',
url: 'https://www.speckle.systems/tutorials/pipe-networks-civil3d-revit'
},
{
title: 'How To Get Data From Grasshopper Into Power BI',
image:
@@ -42,11 +48,5 @@ export const tutorials: TutorialItem[] = [
image:
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c50ac50a28b58fe0e10b_66e047f505114bd4a6854b6a_216-blocks-to-families%25400.5x.png',
url: 'https://www.speckle.systems/tutorials/new-in-2-16-block-to-family-conversion'
},
{
title: 'SketchUp Connector for Mac',
image:
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c508c50a28b58fe0ddef_66e047f6aacfa88a6524a956_final-blog.jpeg',
url: 'https://www.speckle.systems/tutorials/sketchup-connector-for-mac'
}
]
@@ -0,0 +1,13 @@
<template>
<TutorialsPage />
</template>
<script setup lang="ts">
useHead({
title: 'Tutorials'
})
definePageMeta({
middleware: ['auth']
})
</script>
@@ -2036,7 +2036,7 @@ export type Project = {
versions: VersionCollection;
/** Return metadata about resources being requested in the viewer */
viewerResources: Array<ViewerResourceGroup>;
visibility: ProjectVisibility;
visibility: SimpleProjectVisibility;
webhooks: WebhookCollection;
workspace?: Maybe<Workspace>;
workspaceId?: Maybe<Scalars['String']['output']>;
@@ -3147,6 +3147,13 @@ export type SetPrimaryUserEmailInput = {
id: Scalars['ID']['input'];
};
/** Visbility without the "discoverable" option */
export const SimpleProjectVisibility = {
Private: 'PRIVATE',
Unlisted: 'UNLISTED'
} as const;
export type SimpleProjectVisibility = typeof SimpleProjectVisibility[keyof typeof SimpleProjectVisibility];
export type SmartTextEditorValue = {
__typename?: 'SmartTextEditorValue';
/** File attachments, if any */
@@ -5160,6 +5167,7 @@ export type ResolversTypes = {
ServerWorkspacesInfo: ResolverTypeWrapper<GraphQLEmptyReturn>;
SessionPaymentStatus: SessionPaymentStatus;
SetPrimaryUserEmailInput: SetPrimaryUserEmailInput;
SimpleProjectVisibility: SimpleProjectVisibility;
SmartTextEditorValue: ResolverTypeWrapper<SmartTextEditorValueGraphQLReturn>;
SortDirection: SortDirection;
Stream: ResolverTypeWrapper<StreamGraphQLReturn>;
@@ -6328,7 +6336,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
version?: Resolver<ResolversTypes['Version'], ParentType, ContextType, RequireFields<ProjectVersionArgs, 'id'>>;
versions?: Resolver<ResolversTypes['VersionCollection'], ParentType, ContextType, RequireFields<ProjectVersionsArgs, 'limit'>>;
viewerResources?: Resolver<Array<ResolversTypes['ViewerResourceGroup']>, ParentType, ContextType, RequireFields<ProjectViewerResourcesArgs, 'loadedVersionsOnly' | 'resourceIdString'>>;
visibility?: Resolver<ResolversTypes['ProjectVisibility'], ParentType, ContextType>;
visibility?: Resolver<ResolversTypes['SimpleProjectVisibility'], ParentType, ContextType>;
webhooks?: Resolver<ResolversTypes['WebhookCollection'], ParentType, ContextType, Partial<ProjectWebhooksArgs>>;
workspace?: Resolver<Maybe<ResolversTypes['Workspace']>, ParentType, ContextType>;
workspaceId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2016,7 +2016,7 @@ export type Project = {
versions: VersionCollection;
/** Return metadata about resources being requested in the viewer */
viewerResources: Array<ViewerResourceGroup>;
visibility: ProjectVisibility;
visibility: SimpleProjectVisibility;
webhooks: WebhookCollection;
workspace?: Maybe<Workspace>;
workspaceId?: Maybe<Scalars['String']['output']>;
@@ -3127,6 +3127,13 @@ export type SetPrimaryUserEmailInput = {
id: Scalars['ID']['input'];
};
/** Visbility without the "discoverable" option */
export const SimpleProjectVisibility = {
Private: 'PRIVATE',
Unlisted: 'UNLISTED'
} as const;
export type SimpleProjectVisibility = typeof SimpleProjectVisibility[keyof typeof SimpleProjectVisibility];
export type SmartTextEditorValue = {
__typename?: 'SmartTextEditorValue';
/** File attachments, if any */
@@ -4914,7 +4921,7 @@ export type CrossSyncProjectMetadataQueryVariables = Exact<{
}>;
export type CrossSyncProjectMetadataQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, versions: { __typename?: 'VersionCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Version', id: string, createdAt: string, model: { __typename?: 'Model', id: string, name: string } }> } } };
export type CrossSyncProjectMetadataQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: SimpleProjectVisibility, versions: { __typename?: 'VersionCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Version', id: string, createdAt: string, model: { __typename?: 'Model', id: string, name: string } }> } } };
export type CrossSyncClientTestQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2017,7 +2017,7 @@ export type Project = {
versions: VersionCollection;
/** Return metadata about resources being requested in the viewer */
viewerResources: Array<ViewerResourceGroup>;
visibility: ProjectVisibility;
visibility: SimpleProjectVisibility;
webhooks: WebhookCollection;
workspace?: Maybe<Workspace>;
workspaceId?: Maybe<Scalars['String']['output']>;
@@ -3128,6 +3128,13 @@ export type SetPrimaryUserEmailInput = {
id: Scalars['ID']['input'];
};
/** Visbility without the "discoverable" option */
export const SimpleProjectVisibility = {
Private: 'PRIVATE',
Unlisted: 'UNLISTED'
} as const;
export type SimpleProjectVisibility = typeof SimpleProjectVisibility[keyof typeof SimpleProjectVisibility];
export type SmartTextEditorValue = {
__typename?: 'SmartTextEditorValue';
/** File attachments, if any */
@@ -5152,7 +5159,7 @@ export type UpdateWorkspaceProjectRoleMutationVariables = Exact<{
}>;
export type UpdateWorkspaceProjectRoleMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', projects: { __typename?: 'WorkspaceProjectMutations', updateRole: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string } } } };
export type UpdateWorkspaceProjectRoleMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', projects: { __typename?: 'WorkspaceProjectMutations', updateRole: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: SimpleProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string } } } };
export type BasicStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name: string } };
@@ -5495,7 +5502,7 @@ export type EditProjectCommentMutationVariables = Exact<{
export type EditProjectCommentMutation = { __typename?: 'Mutation', commentMutations: { __typename?: 'CommentMutations', edit: { __typename?: 'Comment', id: string, rawText: string, authorId: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null } } } };
export type BasicProjectFieldsFragment = { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string };
export type BasicProjectFieldsFragment = { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: SimpleProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string };
export type AdminProjectListQueryVariables = Exact<{
query?: InputMaybe<Scalars['String']['input']>;
@@ -5506,7 +5513,7 @@ export type AdminProjectListQueryVariables = Exact<{
}>;
export type AdminProjectListQuery = { __typename?: 'Query', admin: { __typename?: 'AdminQueries', projectList: { __typename?: 'ProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string }> } } };
export type AdminProjectListQuery = { __typename?: 'Query', admin: { __typename?: 'AdminQueries', projectList: { __typename?: 'ProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, name: string, description?: string | null, visibility: SimpleProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string }> } } };
export type GetProjectObjectQueryVariables = Exact<{
projectId: Scalars['String']['input'];
@@ -5528,7 +5535,7 @@ export type CreateProjectMutationVariables = Exact<{
}>;
export type CreateProjectMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', create: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string } } };
export type CreateProjectMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', create: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: SimpleProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string } } };
export type BatchDeleteProjectsMutationVariables = Exact<{
ids: Array<Scalars['String']['input']> | Scalars['String']['input'];
@@ -5542,7 +5549,7 @@ export type UpdateProjectRoleMutationVariables = Exact<{
}>;
export type UpdateProjectRoleMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', updateRole: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string } } };
export type UpdateProjectRoleMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', updateRole: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: SimpleProjectVisibility, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string } } };
export type CreateServerInviteMutationVariables = Exact<{
input: ServerInviteCreateInput;