From bb0efade72f0af77a8653d2292ab917ec12284bd Mon Sep 17 00:00:00 2001 From: sercan Date: Sun, 25 Apr 2021 23:23:23 +0300 Subject: [PATCH] (apps/academy) New version of the Academy app --- src/@fuse/tailwind/plugins/utilities.js | 2 +- src/app/app.routing.ts | 1 + src/app/mock-api/apps/academy/api.ts | 86 +++ src/app/mock-api/apps/academy/data.ts | 719 ++++++++++++++++++ src/app/mock-api/common/navigation/data.ts | 15 +- src/app/mock-api/index.ts | 2 + .../admin/apps/academy/academy.component.html | 1 + .../admin/apps/academy/academy.component.ts | 17 + .../admin/apps/academy/academy.module.ts | 44 ++ .../admin/apps/academy/academy.resolvers.ts | 110 +++ .../admin/apps/academy/academy.routing.ts | 32 + .../admin/apps/academy/academy.service.ts | 91 +++ .../admin/apps/academy/academy.types.ts | 29 + .../academy/details/details.component.html | 195 +++++ .../apps/academy/details/details.component.ts | 204 +++++ .../apps/academy/list/list.component.html | 196 +++++ .../admin/apps/academy/list/list.component.ts | 159 ++++ tailwind.config.js | 2 +- 18 files changed, 1902 insertions(+), 3 deletions(-) create mode 100644 src/app/mock-api/apps/academy/api.ts create mode 100644 src/app/mock-api/apps/academy/data.ts create mode 100644 src/app/modules/admin/apps/academy/academy.component.html create mode 100644 src/app/modules/admin/apps/academy/academy.component.ts create mode 100644 src/app/modules/admin/apps/academy/academy.module.ts create mode 100644 src/app/modules/admin/apps/academy/academy.resolvers.ts create mode 100644 src/app/modules/admin/apps/academy/academy.routing.ts create mode 100644 src/app/modules/admin/apps/academy/academy.service.ts create mode 100644 src/app/modules/admin/apps/academy/academy.types.ts create mode 100644 src/app/modules/admin/apps/academy/details/details.component.html create mode 100644 src/app/modules/admin/apps/academy/details/details.component.ts create mode 100644 src/app/modules/admin/apps/academy/list/list.component.html create mode 100644 src/app/modules/admin/apps/academy/list/list.component.ts diff --git a/src/@fuse/tailwind/plugins/utilities.js b/src/@fuse/tailwind/plugins/utilities.js index 2e8df2d4..6c1ecf20 100644 --- a/src/@fuse/tailwind/plugins/utilities.js +++ b/src/@fuse/tailwind/plugins/utilities.js @@ -56,7 +56,7 @@ const utilities = plugin(({ } }, { - variants: ['dark', 'responsive'] + variants: ['dark', 'responsive', 'group-hover', 'hover'] } ); diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 54c4863e..f3624eee 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -82,6 +82,7 @@ export const appRoutes: Route[] = [ // Apps {path: 'apps', children: [ + {path: 'academy', loadChildren: () => import('app/modules/admin/apps/academy/academy.module').then(m => m.AcademyModule)}, {path: 'calendar', loadChildren: () => import('app/modules/admin/apps/calendar/calendar.module').then(m => m.CalendarModule)}, {path: 'contacts', loadChildren: () => import('app/modules/admin/apps/contacts/contacts.module').then(m => m.ContactsModule)}, {path: 'ecommerce', loadChildren: () => import('app/modules/admin/apps/ecommerce/ecommerce.module').then(m => m.ECommerceModule)}, diff --git a/src/app/mock-api/apps/academy/api.ts b/src/app/mock-api/apps/academy/api.ts new file mode 100644 index 00000000..862282c0 --- /dev/null +++ b/src/app/mock-api/apps/academy/api.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@angular/core'; +import { cloneDeep } from 'lodash-es'; +import { FuseMockApiService } from '@fuse/lib/mock-api/mock-api.service'; +import { categories as categoriesData, courses as coursesData, demoCourseSteps as demoCourseStepsData } from 'app/mock-api/apps/academy/data'; + +@Injectable({ + providedIn: 'root' +}) +export class AcademyMockApi +{ + private _categories: any[] = categoriesData; + private _courses: any[] = coursesData; + private _demoCourseSteps: any[] = demoCourseStepsData; + + /** + * Constructor + */ + constructor(private _fuseMockApiService: FuseMockApiService) + { + // Register Mock API handlers + this.registerHandlers(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Register Mock API handlers + */ + registerHandlers(): void + { + // ----------------------------------------------------------------------------------------------------- + // @ Categories - GET + // ----------------------------------------------------------------------------------------------------- + this._fuseMockApiService + .onGet('api/apps/academy/categories') + .reply(() => { + + // Clone the categories + const categories = cloneDeep(this._categories); + + // Sort the categories alphabetically by title + categories.sort((a, b) => a.title.localeCompare(b.title)); + + return [200, categories]; + }); + + // ----------------------------------------------------------------------------------------------------- + // @ Courses - GET + // ----------------------------------------------------------------------------------------------------- + this._fuseMockApiService + .onGet('api/apps/academy/courses') + .reply(() => { + + // Clone the courses + const courses = cloneDeep(this._courses); + + return [200, courses]; + }); + + // ----------------------------------------------------------------------------------------------------- + // @ Course - GET + // ----------------------------------------------------------------------------------------------------- + this._fuseMockApiService + .onGet('api/apps/academy/courses/course') + .reply(({request}) => { + + // Get the id from the params + const id = request.params.get('id'); + + // Clone the courses and steps + const courses = cloneDeep(this._courses); + const steps = cloneDeep(this._demoCourseSteps); + + // Find the course and attach steps to it + const course = courses.find((item) => item.id === id); + course.steps = steps; + + return [ + 200, + course + ]; + }); + } +} diff --git a/src/app/mock-api/apps/academy/data.ts b/src/app/mock-api/apps/academy/data.ts new file mode 100644 index 00000000..8b1c35d4 --- /dev/null +++ b/src/app/mock-api/apps/academy/data.ts @@ -0,0 +1,719 @@ +/* tslint:disable:max-line-length */ +export const categories = [ + { + id : '9a67dff7-3c38-4052-a335-0cef93438ff6', + title: 'Web', + slug : 'web' + }, + { + id : 'a89672f5-e00d-4be4-9194-cb9d29f82165', + title: 'Firebase', + slug : 'firebase' + }, + { + id : '02f42092-bb23-4552-9ddb-cfdcc235d48f', + title: 'Cloud', + slug : 'cloud' + }, + { + id : '5648a630-979f-4403-8c41-fc9790dea8cd', + title: 'Android', + slug : 'android' + } +]; +export const courses = [ + { + id : '694e4e5f-f25f-470b-bd0e-26b1d4f64028', + title : 'Basics of Angular', + slug : 'basics-of-angular', + description: 'Introductory course for Angular and framework basics', + category : 'web', + duration : 30, + totalSteps : 11, + updatedAt : 'Jun 28, 2021', + featured : true, + progress : { + currentStep: 3, + completed : 2 + } + }, + { + id : 'f924007a-2ee9-470b-a316-8d21ed78277f', + title : 'Basics of TypeScript', + slug : 'basics-of-typeScript', + description: 'Beginner course for Typescript and its basics', + category : 'web', + duration : 60, + totalSteps : 11, + updatedAt : 'Nov 01, 2021', + featured : true, + progress : { + currentStep: 5, + completed : 3 + } + }, + { + id : '0c06e980-abb5-4ba7-ab65-99a228cab36b', + title : 'Android N: Quick Settings', + slug : 'android-n-quick-settings', + description: 'Step by step guide for Android N: Quick Settings', + category : 'android', + duration : 120, + totalSteps : 11, + updatedAt : 'May 08, 2021', + featured : false, + progress : { + currentStep: 10, + completed : 1 + } + }, + { + id : '1b9a9acc-9a36-403e-a1e7-b11780179e38', + title : 'Build an App for the Google Assistant with Firebase', + slug : 'build-an-app-for-the-google-assistant-with-firebase', + description: 'Dive deep into Google Assistant apps using Firebase', + category : 'firebase', + duration : 30, + totalSteps : 11, + updatedAt : 'Jan 09, 2021', + featured : false, + progress : { + currentStep: 4, + completed : 3 + } + }, + { + id : '55eb415f-3f4e-4853-a22b-f0ae91331169', + title : 'Keep Sensitive Data Safe and Private', + slug : 'keep-sensitive-data-safe-and-private', + description: 'Learn how to keep your important data safe and private', + category : 'android', + duration : 45, + totalSteps : 11, + updatedAt : 'Jan 14, 2021', + featured : false, + progress : { + currentStep: 6, + completed : 0 + } + }, + { + id : 'fad2ab23-1011-4028-9a54-e52179ac4a50', + title : 'Manage Your Pivotal Cloud Foundry App\'s Using Apigee Edge', + slug : 'manage-your-pivotal-cloud-foundry-apps-using-apigee-Edge', + description: 'Introductory course for Pivotal Cloud Foundry App', + category : 'cloud', + duration : 90, + totalSteps : 11, + updatedAt : 'Jun 24, 2021', + featured : false, + progress : { + currentStep: 6, + completed : 0 + } + }, + { + id : 'c4bc107b-edc4-47a7-a7a8-4fb09732e794', + title : 'Build a PWA Using Workbox', + slug : 'build-a-pwa-using-workbox', + description: 'Step by step guide for building a PWA using Workbox', + category : 'web', + duration : 120, + totalSteps : 11, + updatedAt : 'Nov 19, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : '1449f945-d032-460d-98e3-406565a22293', + title : 'Cloud Functions for Firebase', + slug : 'cloud-functions-for-firebase', + description: 'Beginners guide of Firebase Cloud Functions', + category : 'firebase', + duration : 45, + totalSteps : 11, + updatedAt : 'Jul 11, 2021', + featured : false, + progress : { + currentStep: 3, + completed : 1 + } + }, + { + id : 'f05e08ab-f3e3-4597-a032-6a4b69816f24', + title : 'Building a gRPC Service with Java', + slug : 'building-a-grpc-service-with-java', + description: 'Learn more about building a gRPC Service with Java', + category : 'cloud', + duration : 30, + totalSteps : 11, + updatedAt : 'Mar 13, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 1 + } + }, + { + id : '181728f4-87c8-45c5-b9cc-92265bcd2f4d', + title : 'Looking at Campaign Finance with BigQuery', + slug : 'looking-at-campaign-finance-with-bigquery', + description: 'Dive deep into BigQuery: Campaign Finance', + category : 'cloud', + duration : 60, + totalSteps : 11, + updatedAt : 'Nov 01, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : 'fcbfedbf-6187-4b3b-89d3-1a7cb4e11616', + title : 'Personalize Your iOS App with Firebase User Management', + slug : 'personalize-your-ios-app-with-firebase-user-management', + description: 'Dive deep into User Management on iOS apps using Firebase', + category : 'firebase', + duration : 90, + totalSteps : 11, + updatedAt : 'Aug 08, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : '5213f6a1-1dd7-4b1d-b6e9-ffb7af534f28', + title : 'Customize Network Topology with Subnetworks', + slug : 'customize-network-topology-with-subnetworks', + description: 'Dive deep into Network Topology with Subnetworks', + category : 'web', + duration : 45, + totalSteps : 11, + updatedAt : 'May 12, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : '02992ac9-d1a3-4167-b70e-8a1d5b5ba253', + title : 'Building Beautiful UIs with Flutter', + slug : 'building-beautiful-uis-with-flutter', + description: 'Dive deep into Flutter\'s hidden secrets for creating beautiful UIs', + category : 'web', + duration : 90, + totalSteps : 11, + updatedAt : 'Sep 18, 2021', + featured : false, + progress : { + currentStep: 8, + completed : 2 + } + }, + { + id : '2139512f-41fb-4a4a-841a-0b4ac034f9b4', + title : 'Firebase Android', + slug : 'firebase-android', + description: 'Beginners guide of Firebase for Android', + category : 'android', + duration : 45, + totalSteps : 11, + updatedAt : 'Apr 24, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : '65e0a0e0-d8c0-4117-a3cb-eb74f8e28809', + title : 'Simulating a Thread Network Using OpenThread', + slug : 'simulating-a-thread-network-using-openthread', + description: 'Introductory course for OpenThread and Simulating a Thread Network', + category : 'web', + duration : 45, + totalSteps : 11, + updatedAt : 'Jun 05, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : 'c202ebc9-9be3-433a-9d38-7003b3ed7b7a', + title : 'Your First Progressive Web App', + slug : 'your-first-progressive-web-app', + description: 'Step by step guide for creating a PWA from scratch', + category : 'web', + duration : 30, + totalSteps : 11, + updatedAt : 'Oct 14, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : '980ae7da-9f77-4e30-aa98-1b1ea594e775', + title : 'Launch Cloud Datalab', + slug : 'launch-cloud-datalab', + description: 'From start to finish: Launch Cloud Datalab', + category : 'cloud', + duration : 60, + totalSteps : 11, + updatedAt : 'Dec 16, 2021', + featured : false, + progress : { + currentStep: 0, + completed : 0 + } + }, + { + id : 'c9748ea9-4117-492c-bdb2-55085b515978', + title : 'Cloud Firestore', + slug : 'cloud-firestore', + description: 'Step by step guide for setting up Cloud Firestore', + category : 'firebase', + duration : 90, + totalSteps : 11, + updatedAt : 'Apr 04, 2021', + featured : false, + progress : { + currentStep: 2, + completed : 0 + } + } +]; +export const demoCourseContent = ` +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus aperiam lab et fugiat id magnam minus nemo quam + voluptatem. Culpa deleniti explica nisi quod soluta. +

+

+ Alias animi labque, deserunt distinctio eum excepturi fuga iure labore magni molestias mollitia natus, officia pofro + quis sunt temporibus veritatis voluptatem, voluptatum. Aut blanditiis esse et illum maxim, obcaecati possimus + voluptate! Accusamus adipisci amet aperiam, assumenda consequuntur fugiat inventore iusto magnam molestias + natus necessitatibus, nulla pariatur. +

+

+ Amet distinctio enim itaque minima minus nesciunt recusandae soluta voluptatibus: +

+
+

+ Ad aliquid amet asperiores lab distinctio doloremque eaque, exercitationem explicabo, minus mollitia + natus necessitatibus odio omnis pofro rem. +

+
+

+ Alias architecto asperiores, dignissimos illum ipsam ipsum itaque, natus necessitatibus officiis, perferendis quae + sed ullam veniam vitae voluptas! Magni, nisi, quis! A accusamus animi commodi, consectetur distinctio + eaque, eos excepturi illum laboriosam maiores nam natus nulla officiis perspiciatis rem reprehenderit sed + tenetur veritatis. +

+

+ Consectetur dicta enim error eveniet expedita, facere in itaque labore natus quasi? Ad consectetur + eligendi facilis magni quae quis, quo temporibus voluptas voluptate voluptatem! +

+

+ Adipisci alias animi debitis eos et impedit maiores, modi nam nobis officia optio perspiciatis, rerum. + Accusantium esse nostrum odit quis quo: +

+
h1 a {{'{'}}
+    display: block;
+    width: 300px;
+    height: 80px;
+{{'}'}}
+

+ Accusantium aut autem, lab deleniti eaque fugiat fugit id ipsa iste molestiae, + necessitatibus nemo quasi + . +

+
+

+ Accusantium aspernatur autem enim +

+

+ Blanditiis, fugit voluptate! Assumenda blanditiis consectetur, labque cupiditate ducimus eaque earum, fugiat illum + ipsa, necessitatibus omnis quaerat reiciendis totam. Architecto, facere illum molestiae nihil nulla + quibusdam quidem vel! Atque blanditiis deserunt. +

+

+ Debitis deserunt doloremque labore laboriosam magni minus odit: +

+
    +
  1. Asperiores dicta esse maiores nobis officiis.
  2. +
  3. Accusamus aliquid debitis dolore illo ipsam molettiae possimus.
  4. +
  5. Magnam mollitia pariatur perspiciatis quasi quidem tenetur voluptatem! Adipisci aspernatur assumenda dicta.
  6. +
+

+ Animi fugit incidunt iure magni maiores molestias. +

+

+ Consequatur iusto soluta +

+

+ Aliquid asperiores corporis — deserunt dolorum ducimus eius eligendi explicabo quaerat suscipit voluptas. +

+

+ Deserunt dolor eos et illum laborum magni molestiae mollitia: +

+
+

Autem beatae consectetur consequatur, facere, facilis fugiat id illo, impedit numquam optio quis sunt ducimus illo.

+
+

+ Adipisci consequuntur doloribus facere in ipsam maxime molestias pofro quam: +

+
+ +
+ Accusamus blanditiis labque delectus esse et eum excepturi, impedit ipsam iste maiores minima mollitia, nihil obcaecati + placeat quaerat qui quidem sint unde! +
+
+

+ A beatae lab deleniti explicabo id inventore magni nisi omnis placeat praesentium quibusdam: +

+ +

+ Consequ eius eum excepturi explicabo. +

+

+ Adipisicing elit atque impedit? +

+

+ Atque distinctio doloremque ea qui quo, repellendus. +

+

+ Delectus deserunt explicabo facilis numquam quasi! Laboriosam, magni, quisquam. Aut, blanditiis commodi distinctio, facere fuga + hic itaque iure labore laborum maxime nemo neque provident quos recusandae sequi veritatis illum inventore iure qui rerum sapiente. +

+

+ Accusamus iusto sint aperiam consectetur … +

+

+ Aliquid assumenda ipsa nam odit pofro quaerat, quasi recusandae sint! Aut, esse explicabo facilis fugit illum iure magni + necessitatibus odio quas. +

+ +

+ Animi aperiam autem labque dolore enim ex expedita harum hic id impedit ipsa laborum modi mollitia non perspiciatis quae ratione. +

+

+ Alias eos excepturi facilis fugit. +

+

+ Alias asperiores, aspernatur corporis + delectus + est + facilis + inventore dolore + ipsa nobis nostrum officia quia, veritatis vero! At dolore est nesciunt numquam quam. Ab animi architecto aut, dignissimos + eos est eum explicabo. +

+

+ Adipisci autem consequuntur labque cupiditate dolor ducimus fuga neque nesciunt: +

+
module.exports = {{'{'}}
+    purge: [],
+    theme: {{'{'}}
+        extend: {{'{}'}},
+    },
+    variants: {{'{}'}},
+    plugins: [],
+{{'}'}}
+

+ Aliquid aspernatur eius fugit hic iusto. +

+

+ Dolorum ducimus expedita? +

+

+ Culpa debitis explicabo maxime minus quaerat reprehenderit temporibus! Asperiores, cupiditate ducimus esse est expedita fuga hic + ipsam necessitatibus placeat possimus? Amet animi aut consequuntur earum eveniet. +

+
    +
  1. + Aspernatur at beatae corporis debitis. +
      +
    • + Aperiam assumenda commodi lab dicta eius, “fugit ipsam“ itaque iure molestiae nihil numquam, officia omnis quia + repellendus sapiente sed. +
    • +
    • + Nulla odio quod saepe accusantium, adipisci autem blanditiis lab doloribus. +
    • +
    • + Explicabo facilis iusto molestiae nisi nostrum obcaecati officia. +
    • +
    +
  2. +
  3. + Nobis odio officiis optio quae quis quisquam quos rem. +
      +
    • Modi pariatur quod totam. Deserunt doloribus eveniet, expedita.
    • +
    • Ad beatae dicta et fugit libero optio quaerat rem repellendus./
    • +
    • Architecto atque consequuntur corporis id iste magni.
    • +
    +
  4. +
  5. + Deserunt non placeat unde veniam veritatis? Odio quod. +
      +
    • Inventore iure magni quod repellendus tempora. Magnam neque, quia. Adipisci amet.
    • +
    • Consectetur adipisicing elit.
    • +
    • labque eum expedita illo inventore iusto laboriosam nesciunt non, odio provident.
    • +
    +
  6. +
+

+ A aliquam architecto consequatur labque dicta doloremque <li> doloribus, ducimus earum, est <p> + eveniet explicabo fuga fugit ipsum minima minus molestias nihil nisi non qui sunt vel voluptatibus? A dolorum illum nihil quidem. +

+ +

+ Consectetur adipisicing elit dicta dolor iste. +

+

+ Consectetur ea natus officia omnis reprehenderit. +

+

+ Distinctio impedit quaerat sed! Accusamus + aliquam aspernatur enim expedita explicabo + . Libero molestiae + odio quasi unde ut? Ab exercitationem id numquam odio quisquam! +

+

+ Explicabo facilis nemo quidem natus tempore: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WrestlerOriginFinisher
Bret “The Hitman” HartCalgary, ABSharpshooter
Stone Cold Steve AustinAustin, TXStone Cold Stunner
Randy SavageSarasota, FLElbow Drop
VaderBoulder, COVader Bomb
Razor RamonChuluota, FLRazor’s Edge
+

+ A aliquid autem lab doloremque, ea earum eum fuga fugit illo ipsa minus natus nisi <span> obcaecati pariatur + perferendis pofro suscipit tempore. +

+

+ Ad alias atque culpa illum earum optio +

+

+ Architecto consequatur eveniet illo in iure laborum minus omnis quibusdam sequi temporibus? Ab aliquid “atque dolores molestiae + nemo perferendis” reprehenderit saepe. +

+

+ Accusantium aliquid eligendi est fuga natus, quos vel? Adipisci aperiam asperiores aspernatur consectetur cupiditate + @distinctio/doloribus + et exercitationem expedita, facere facilis illum, impedit inventore + ipsa iure iusto magnam, magni minus nesciunt non officia possimus quod reiciendis. +

+

+ Cupiditate explicabo hic maiores +

+

+ Aliquam amet consequuntur distinctio ea est excepturi facere illum maiores nisi nobis non odit officiis + quisquam, similique tempora temporibus, tenetur ullam voluptates adipisci aperiam deleniti doloremque + ducimus eos. +

+

+ Ducimus qui quo tempora. lab enim explicabo hic inventore qui soluta voluptates voluptatum? Asperiores consectetur + delectus dolorem fugiat ipsa pariatur, quas quos repellendus repudiandae sunt aut blanditiis. +

+

+ Asperiores aspernatur autem error praesentium quidem. +

+

+ Ad blanditiis commodi, doloribus id iste repudiandae vero vitae. +

+

+ Atque consectetur lab debitis enim est et, facere fugit impedit, possimus quaerat quibusdam. +

+

+ Dolorem nihil placeat quibusdam veniam? Amet architecto at consequatur eligendi eveniet excepturi hic illo impedit in iste magni maxime + modi nisi nulla odio placeat quidem, quos rem repellat similique suscipit voluptate voluptates nobis omnis quo repellendus. +

+

+ Assumenda, eum, minima! Autem consectetur fugiat iste sit! Nobis omnis quo repellendus. +

+`; +export const demoCourseSteps = [ + { + order : 0, + title : 'Introduction', + subtitle: 'Introducing the library and how it works', + content : `

Introduction

${demoCourseContent}` + }, + { + order : 1, + title : 'Get the sample code', + subtitle: 'Where to find the sample code and how to access it', + content : `

Get the sample code

${demoCourseContent}` + }, + { + order : 2, + title : 'Create a Firebase project and Set up your app', + subtitle: 'How to create a basic Firebase project and how to run it locally', + content : `

Create a Firebase project and Set up your app

${demoCourseContent}` + }, + { + order : 3, + title : 'Install the Firebase Command Line Interface', + subtitle: 'Setting up the Firebase CLI to access command line tools', + content : `

Install the Firebase Command Line Interface

${demoCourseContent}` + }, + { + order : 4, + title : 'Deploy and run the web app', + subtitle: 'How to build, push and run the project remotely', + content : `

Deploy and run the web app

${demoCourseContent}` + }, + { + order : 5, + title : 'The Functions Directory', + subtitle: 'Introducing the Functions and Functions Directory', + content : `

The Functions Directory

${demoCourseContent}` + }, + { + order : 6, + title : 'Import the Cloud Functions and Firebase Admin modules', + subtitle: 'Create your first Function and run it to administer your app', + content : `

Import the Cloud Functions and Firebase Admin modules

${demoCourseContent}` + }, + { + order : 7, + title : 'Welcome New Users', + subtitle: 'How to create a welcome message for the new users', + content : `

Welcome New Users

${demoCourseContent}` + }, + { + order : 8, + title : 'Images moderation', + subtitle: 'How to moderate images; crop, resize, optimize', + content : `

Images moderation

${demoCourseContent}` + }, + { + order : 9, + title : 'New Message Notifications', + subtitle: 'How to create and push a notification to a user', + content : `

New Message Notifications

${demoCourseContent}` + }, + { + order : 10, + title : 'Congratulations!', + subtitle: 'Nice work, you have created your first application', + content : `

Congratulations!

${demoCourseContent}` + } +]; diff --git a/src/app/mock-api/common/navigation/data.ts b/src/app/mock-api/common/navigation/data.ts index 5345197f..21bd1c67 100644 --- a/src/app/mock-api/common/navigation/data.ts +++ b/src/app/mock-api/common/navigation/data.ts @@ -1,4 +1,3 @@ - /* tslint:disable:max-line-length */ import { FuseNavigationItem } from '@fuse/components/navigation'; @@ -33,6 +32,13 @@ export const defaultNavigation: FuseNavigationItem[] = [ type : 'group', icon : 'heroicons_outline:home', children: [ + { + id : 'apps.academy', + title: 'Academy', + type : 'basic', + icon : 'heroicons_outline:academic-cap', + link : '/apps/academy' + }, { id : 'apps.calendar', title : 'Calendar', @@ -1135,6 +1141,13 @@ export const futuristicNavigation: FuseNavigationItem[] = [ icon : 'heroicons_outline:clipboard-check', link : '/dashboards/project' }, + { + id : 'apps.academy', + title: 'Academy', + type : 'basic', + icon : 'heroicons_outline:academic-cap', + link : '/apps/academy' + }, { id : 'apps.calendar', title: 'Calendar', diff --git a/src/app/mock-api/index.ts b/src/app/mock-api/index.ts index 96b574c3..0b83a2f3 100644 --- a/src/app/mock-api/index.ts +++ b/src/app/mock-api/index.ts @@ -1,3 +1,4 @@ +import { AcademyMockApi } from 'app/mock-api/apps/academy/api'; import { AnalyticsMockApi } from 'app/mock-api/dashboards/analytics/api'; import { AuthMockApi } from 'app/mock-api/common/auth/api'; import { CalendarMockApi } from 'app/mock-api/apps/calendar/api'; @@ -17,6 +18,7 @@ import { TasksMockApi } from 'app/mock-api/apps/tasks/api'; import { UserMockApi } from 'app/mock-api/common/user/api'; export const mockApiServices = [ + AcademyMockApi, AnalyticsMockApi, AuthMockApi, CalendarMockApi, diff --git a/src/app/modules/admin/apps/academy/academy.component.html b/src/app/modules/admin/apps/academy/academy.component.html new file mode 100644 index 00000000..0680b43f --- /dev/null +++ b/src/app/modules/admin/apps/academy/academy.component.html @@ -0,0 +1 @@ + diff --git a/src/app/modules/admin/apps/academy/academy.component.ts b/src/app/modules/admin/apps/academy/academy.component.ts new file mode 100644 index 00000000..6134aa35 --- /dev/null +++ b/src/app/modules/admin/apps/academy/academy.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector : 'academy', + templateUrl : './academy.component.html', + encapsulation : ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AcademyComponent +{ + /** + * Constructor + */ + constructor() + { + } +} diff --git a/src/app/modules/admin/apps/academy/academy.module.ts b/src/app/modules/admin/apps/academy/academy.module.ts new file mode 100644 index 00000000..4ae79191 --- /dev/null +++ b/src/app/modules/admin/apps/academy/academy.module.ts @@ -0,0 +1,44 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { FuseFindByKeyPipeModule } from '@fuse/pipes/find-by-key'; +import { SharedModule } from 'app/shared/shared.module'; +import { academyRoutes } from 'app/modules/admin/apps/academy/academy.routing'; +import { AcademyComponent } from 'app/modules/admin/apps/academy/academy.component'; +import { AcademyDetailsComponent } from 'app/modules/admin/apps/academy/details/details.component'; +import { AcademyListComponent } from 'app/modules/admin/apps/academy/list/list.component'; +import { MatTabsModule } from '@angular/material/tabs'; + +@NgModule({ + declarations: [ + AcademyComponent, + AcademyDetailsComponent, + AcademyListComponent + ], + imports: [ + RouterModule.forChild(academyRoutes), + MatButtonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatProgressBarModule, + MatSelectModule, + MatSidenavModule, + MatSlideToggleModule, + MatTooltipModule, + FuseFindByKeyPipeModule, + SharedModule, + MatTabsModule + ] +}) +export class AcademyModule +{ +} diff --git a/src/app/modules/admin/apps/academy/academy.resolvers.ts b/src/app/modules/admin/apps/academy/academy.resolvers.ts new file mode 100644 index 00000000..999046b4 --- /dev/null +++ b/src/app/modules/admin/apps/academy/academy.resolvers.ts @@ -0,0 +1,110 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { Category, Course } from 'app/modules/admin/apps/academy/academy.types'; +import { AcademyService } from 'app/modules/admin/apps/academy/academy.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AcademyCategoriesResolver implements Resolve +{ + /** + * Constructor + */ + constructor(private _academyService: AcademyService) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Resolver + * + * @param route + * @param state + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable + { + return this._academyService.getCategories(); + } +} + +@Injectable({ + providedIn: 'root' +}) +export class AcademyCoursesResolver implements Resolve +{ + /** + * Constructor + */ + constructor(private _academyService: AcademyService) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Resolver + * + * @param route + * @param state + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable + { + return this._academyService.getCourses(); + } +} + +@Injectable({ + providedIn: 'root' +}) +export class AcademyCourseResolver implements Resolve +{ + /** + * Constructor + */ + constructor( + private _router: Router, + private _academyService: AcademyService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Resolver + * + * @param route + * @param state + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable + { + return this._academyService.getCourseById(route.paramMap.get('id')) + .pipe( + // Error here means the requested task is not available + catchError((error) => { + + // Log the error + console.error(error); + + // Get the parent url + const parentUrl = state.url.split('/').slice(0, -1).join('/'); + + // Navigate to there + this._router.navigateByUrl(parentUrl); + + // Throw an error + return throwError(error); + }) + ); + } +} diff --git a/src/app/modules/admin/apps/academy/academy.routing.ts b/src/app/modules/admin/apps/academy/academy.routing.ts new file mode 100644 index 00000000..4e5af973 --- /dev/null +++ b/src/app/modules/admin/apps/academy/academy.routing.ts @@ -0,0 +1,32 @@ +import { Route } from '@angular/router'; +import { AcademyComponent } from 'app/modules/admin/apps/academy/academy.component'; +import { AcademyListComponent } from 'app/modules/admin/apps/academy/list/list.component'; +import { AcademyDetailsComponent } from 'app/modules/admin/apps/academy/details/details.component'; +import { AcademyCategoriesResolver, AcademyCourseResolver, AcademyCoursesResolver } from 'app/modules/admin/apps/academy/academy.resolvers'; + +export const academyRoutes: Route[] = [ + { + path : '', + component: AcademyComponent, + resolve : { + categories: AcademyCategoriesResolver + }, + children : [ + { + path : '', + pathMatch: 'full', + component: AcademyListComponent, + resolve : { + courses: AcademyCoursesResolver + } + }, + { + path : ':id', + component: AcademyDetailsComponent, + resolve : { + course: AcademyCourseResolver + } + } + ] + } +]; diff --git a/src/app/modules/admin/apps/academy/academy.service.ts b/src/app/modules/admin/apps/academy/academy.service.ts new file mode 100644 index 00000000..d639b119 --- /dev/null +++ b/src/app/modules/admin/apps/academy/academy.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { Category, Course } from 'app/modules/admin/apps/academy/academy.types'; + +@Injectable({ + providedIn: 'root' +}) +export class AcademyService +{ + // Private + private _categories: BehaviorSubject = new BehaviorSubject(null); + private _course: BehaviorSubject = new BehaviorSubject(null); + private _courses: BehaviorSubject = new BehaviorSubject(null); + + /** + * Constructor + */ + constructor(private _httpClient: HttpClient) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Getter for categories + */ + get categories$(): Observable + { + return this._categories.asObservable(); + } + + /** + * Getter for courses + */ + get courses$(): Observable + { + return this._courses.asObservable(); + } + + /** + * Getter for course + */ + get course$(): Observable + { + return this._course.asObservable(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Get categories + */ + getCategories(): Observable + { + return this._httpClient.get('api/apps/academy/categories').pipe( + tap((response: any) => { + this._categories.next(response); + }) + ); + } + + /** + * Get courses + */ + getCourses(): Observable + { + return this._httpClient.get('api/apps/academy/courses').pipe( + tap((response: any) => { + this._courses.next(response); + }) + ); + } + + /** + * Get course by id + */ + getCourseById(id: string): Observable + { + return this._httpClient.get('api/apps/academy/courses/course', {params: {id}}).pipe( + tap((response: any) => { + this._course.next(response); + }) + ); + } +} diff --git a/src/app/modules/admin/apps/academy/academy.types.ts b/src/app/modules/admin/apps/academy/academy.types.ts new file mode 100644 index 00000000..67de1ac4 --- /dev/null +++ b/src/app/modules/admin/apps/academy/academy.types.ts @@ -0,0 +1,29 @@ +export interface Category +{ + id?: string; + title?: string; + slug?: string; +} + +export interface Course +{ + id?: string; + title?: string; + slug?: string; + description?: string; + category?: string; + duration?: number; + steps?: { + order?: number; + title?: string; + subtitle?: string; + content?: string; + }[]; + totalSteps?: number; + updatedAt?: number; + featured?: boolean; + progress?: { + currentStep?: number; + completed?: number; + }; +} diff --git a/src/app/modules/admin/apps/academy/details/details.component.html b/src/app/modules/admin/apps/academy/details/details.component.html new file mode 100644 index 00000000..c2ad7a57 --- /dev/null +++ b/src/app/modules/admin/apps/academy/details/details.component.html @@ -0,0 +1,195 @@ +
+ + + + + +
+ + + + + Back to courses + + + + +
+ {{category.title}} +
+
+ +
{{course.title}}
+
{{course.description}}
+ +
+ +
{{course.duration}} minutes
+
+
+ + +
+
    + +
  1. + +
    +
    +
    +
    + + + + + + +
    {{step.order + 1}}
    +
    + + +
    {{step.order + 1}}
    +
    +
    +
    +
    {{step.title}}
    +
    {{step.subtitle}}
    +
    +
    +
  2. +
    +
+
+ +
+ + + + + +
+ + +

+ {{course.title}} +

+
+ + + +
+ + + + + + +
+
+
+
+
+ + + + +
+ + +
+ +
+ {{currentStep + 1}} + / + {{course.totalSteps}} +
+ + + +
+ +
+ +
+ +
diff --git a/src/app/modules/admin/apps/academy/details/details.component.ts b/src/app/modules/admin/apps/academy/details/details.component.ts new file mode 100644 index 00000000..994865ee --- /dev/null +++ b/src/app/modules/admin/apps/academy/details/details.component.ts @@ -0,0 +1,204 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { MatTabGroup } from '@angular/material/tabs'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { FuseMediaWatcherService } from '@fuse/services/media-watcher'; +import { Category, Course } from 'app/modules/admin/apps/academy/academy.types'; +import { AcademyService } from 'app/modules/admin/apps/academy/academy.service'; + +@Component({ + selector : 'academy-details', + templateUrl : './details.component.html', + encapsulation : ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AcademyDetailsComponent implements OnInit, OnDestroy +{ + @ViewChild('courseSteps', {static: true}) courseSteps: MatTabGroup; + categories: Category[]; + course: Course; + currentStep: number = 0; + drawerMode: 'over' | 'side' = 'side'; + drawerOpened: boolean = true; + private _unsubscribeAll: Subject = new Subject(); + + /** + * Constructor + */ + constructor( + @Inject(DOCUMENT) private _document: Document, + private _academyService: AcademyService, + private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + private _fuseMediaWatcherService: FuseMediaWatcherService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Lifecycle hooks + // ----------------------------------------------------------------------------------------------------- + + /** + * On init + */ + ngOnInit(): void + { + // Get the categories + this._academyService.categories$ + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe((categories: Category[]) => { + + // Get the categories + this.categories = categories; + + // Mark for check + this._changeDetectorRef.markForCheck(); + }); + + // Get the course + this._academyService.course$ + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe((course: Course) => { + + // Get the course + this.course = course; + + // Go to step + this.goToStep(course.progress.currentStep); + + // Mark for check + this._changeDetectorRef.markForCheck(); + }); + + // Subscribe to media changes + this._fuseMediaWatcherService.onMediaChange$ + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe(({matchingAliases}) => { + + // Set the drawerMode and drawerOpened + if ( matchingAliases.includes('lg') ) + { + this.drawerMode = 'side'; + this.drawerOpened = true; + } + else + { + this.drawerMode = 'over'; + this.drawerOpened = false; + } + + // Mark for check + this._changeDetectorRef.markForCheck(); + }); + } + + /** + * On destroy + */ + ngOnDestroy(): void + { + // Unsubscribe from all subscriptions + this._unsubscribeAll.next(); + this._unsubscribeAll.complete(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Go to given step + * + * @param step + */ + goToStep(step: number): void + { + // Set the current step + this.currentStep = step; + + // Go to the step + this.courseSteps.selectedIndex = this.currentStep; + + // Mark for check + this._changeDetectorRef.markForCheck(); + } + + /** + * Go to previous step + */ + goToPreviousStep(): void + { + // Return if we already on the first step + if ( this.currentStep === 0 ) + { + return; + } + + // Go to step + this.goToStep(this.currentStep - 1); + + // Scroll the current step selector from sidenav into view + this._scrollCurrentStepElementIntoView(); + } + + /** + * Go to next step + */ + goToNextStep(): void + { + // Return if we already on the last step + if ( this.currentStep === this.course.totalSteps - 1 ) + { + return; + } + + // Go to step + this.goToStep(this.currentStep + 1); + + // Scroll the current step selector from sidenav into view + this._scrollCurrentStepElementIntoView(); + } + + /** + * Track by function for ngFor loops + * + * @param index + * @param item + */ + trackByFn(index: number, item: any): any + { + return item.id || index; + } + + // ----------------------------------------------------------------------------------------------------- + // @ Private methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Scrolls the current step element from + * sidenav into the view. This only happens when + * previous/next buttons pressed as we don't want + * to change the scroll position of the sidebar + * when the user actually clicks around the sidebar. + * + * @private + */ + private _scrollCurrentStepElementIntoView(): void + { + // Wrap everything into setTimeout so we can make sure that the 'current-step' class points to correct element + setTimeout(() => { + + // Get the current step element and scroll it into view + const currentStepElement = this._document.getElementsByClassName('current-step')[0]; + if ( currentStepElement ) + { + currentStepElement.scrollIntoView({ + behavior: 'smooth', + block : 'start' + }); + } + }); + } +} diff --git a/src/app/modules/admin/apps/academy/list/list.component.html b/src/app/modules/admin/apps/academy/list/list.component.html new file mode 100644 index 00000000..aaa9953d --- /dev/null +++ b/src/app/modules/admin/apps/academy/list/list.component.html @@ -0,0 +1,196 @@ +
+ + +
+ + + + + + + + + +
+

FUSE ACADEMY

+
+ What do you want to learn today? +
+
+ Our courses will step you through the process of a building small applications, or adding new features to existing applications. +
+
+
+ + +
+ +
+ +
+ + + All + + {{category.title}} + + + + + + + + + Hide completed + +
+ + +
+ + +
+
+
+ + +
+ {{category.title}} +
+
+ +
+ + + +
+
+ +
{{course.title}}
+
{{course.description}}
+
+ +
+ +
{{course.duration}} minutes
+
+ +
+ + +
Never completed
+
+ +
+ Completed + + + once + + twice + + {{course.progress.completed}} + {{course.progress.completed | i18nPlural: { + '=0' : 'time', + '=1' : 'time', + 'other': 'times' + } }} + + +
+
+
+
+ +
+ +
+
+ +
+ + +
+ +
+
+
+
+
+
+ + + +
+ +
No courses found!
+
+
+
+ +
+ +
diff --git a/src/app/modules/admin/apps/academy/list/list.component.ts b/src/app/modules/admin/apps/academy/list/list.component.ts new file mode 100644 index 00000000..2b006898 --- /dev/null +++ b/src/app/modules/admin/apps/academy/list/list.component.ts @@ -0,0 +1,159 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MatSelectChange } from '@angular/material/select'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; +import { BehaviorSubject, combineLatest, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { AcademyService } from 'app/modules/admin/apps/academy/academy.service'; +import { Category, Course } from 'app/modules/admin/apps/academy/academy.types'; + +@Component({ + selector : 'academy-list', + templateUrl : './list.component.html', + encapsulation : ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AcademyListComponent implements OnInit, OnDestroy +{ + categories: Category[]; + courses: Course[]; + filteredCourses: Course[]; + filters: { + categorySlug$: BehaviorSubject; + query$: BehaviorSubject; + hideCompleted$: BehaviorSubject; + } = { + categorySlug$ : new BehaviorSubject('all'), + query$ : new BehaviorSubject(''), + hideCompleted$: new BehaviorSubject(false) + }; + + private _unsubscribeAll: Subject = new Subject(); + + /** + * Constructor + */ + constructor( + private _activatedRoute: ActivatedRoute, + private _changeDetectorRef: ChangeDetectorRef, + private _router: Router, + private _academyService: AcademyService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Lifecycle hooks + // ----------------------------------------------------------------------------------------------------- + + /** + * On init + */ + ngOnInit(): void + { + // Get the categories + this._academyService.categories$ + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe((categories: Category[]) => { + this.categories = categories; + + // Mark for check + this._changeDetectorRef.markForCheck(); + }); + + // Get the courses + this._academyService.courses$ + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe((courses: Course[]) => { + this.courses = this.filteredCourses = courses; + + // Mark for check + this._changeDetectorRef.markForCheck(); + }); + + // Filter the courses + combineLatest([this.filters.categorySlug$, this.filters.query$, this.filters.hideCompleted$]) + .subscribe(([categorySlug, query, hideCompleted]) => { + + // Reset the filtered courses + this.filteredCourses = this.courses; + + // Filter by category + if ( categorySlug !== 'all' ) + { + this.filteredCourses = this.filteredCourses.filter((course) => course.category === categorySlug); + } + + // Filter by search query + if ( query !== '' ) + { + this.filteredCourses = this.filteredCourses.filter((course) => { + return course.title.toLowerCase().includes(query.toLowerCase()) + || course.description.toLowerCase().includes(query.toLowerCase()) + || course.category.toLowerCase().includes(query.toLowerCase()); + }); + } + + // Filter by completed + if ( hideCompleted ) + { + this.filteredCourses = this.filteredCourses.filter((course) => course.progress.completed === 0); + } + }); + } + + /** + * On destroy + */ + ngOnDestroy(): void + { + // Unsubscribe from all subscriptions + this._unsubscribeAll.next(); + this._unsubscribeAll.complete(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Filter by search query + * + * @param query + */ + filterByQuery(query: string): void + { + this.filters.query$.next(query); + } + + /** + * Filter by category + * + * @param change + */ + filterByCategory(change: MatSelectChange): void + { + this.filters.categorySlug$.next(change.value); + } + + /** + * Show/hide completed courses + * + * @param change + */ + toggleCompleted(change: MatSlideToggleChange): void + { + this.filters.hideCompleted$.next(change.checked); + } + + /** + * Track by function for ngFor loops + * + * @param index + * @param item + */ + trackByFn(index: number, item: any): any + { + return item.id || index; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 1a5fa0f1..f2986418 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -412,7 +412,7 @@ const config = { pointerEvents : ['responsive'], position : ['responsive'], resize : [], - ringColor : ['dark'], + ringColor : ['dark', 'group-hover'], ringOffsetColor : ['dark'], ringOffsetWidth : [], ringOpacity : [],