├── .editorconfig ├── .gitignore ├── .prettierrc ├── README.md ├── apps ├── cms │ ├── .eslintrc.js │ ├── .gitignore │ ├── .stylelintrc.json │ ├── README.md │ ├── package.json │ ├── sanity.cli.ts │ ├── sanity.config.ts │ ├── schemaTypes │ │ ├── general │ │ │ ├── category.ts │ │ │ ├── page.ts │ │ │ └── product.ts │ │ ├── global │ │ │ └── settings.ts │ │ ├── index.ts │ │ ├── layout │ │ │ ├── footer.ts │ │ │ └── menu.ts │ │ ├── modules │ │ │ ├── categories.ts │ │ │ ├── columns.ts │ │ │ ├── contact.ts │ │ │ ├── details.ts │ │ │ ├── disclaimer.ts │ │ │ ├── error.ts │ │ │ ├── gallery.ts │ │ │ ├── header.ts │ │ │ ├── hero.ts │ │ │ ├── highlight.ts │ │ │ ├── information.ts │ │ │ ├── ingredients.ts │ │ │ ├── intro.ts │ │ │ ├── list.ts │ │ │ ├── lookbook.ts │ │ │ ├── marquee.ts │ │ │ ├── media.ts │ │ │ ├── quote.ts │ │ │ ├── seasons.ts │ │ │ └── shop.ts │ │ └── shared │ │ │ ├── content.ts │ │ │ ├── link.ts │ │ │ └── social.ts │ ├── structure │ │ └── index.ts │ └── tsconfig.json └── web │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── client.ts │ ├── controllers │ ├── newsletter.ts │ └── page.ts │ ├── download.ts │ ├── index.js │ ├── index.js.map │ ├── index.ts │ ├── nodemon.json │ ├── package.json │ ├── router │ └── index.ts │ ├── src │ ├── app │ │ ├── classes │ │ │ └── Animation.ts │ │ ├── components │ │ │ ├── Menu.ts │ │ │ ├── Navigation.ts │ │ │ └── Transition.ts │ │ ├── datasets │ │ │ ├── Newsletter.ts │ │ │ ├── Paragraph.ts │ │ │ ├── Parallax.ts │ │ │ ├── Reveal.ts │ │ │ ├── Source.ts │ │ │ ├── Title.ts │ │ │ ├── Translate.ts │ │ │ └── sections │ │ │ │ ├── Categories.ts │ │ │ │ ├── Details.ts │ │ │ │ ├── Footer.ts │ │ │ │ ├── Hero.ts │ │ │ │ ├── List.ts │ │ │ │ ├── Marquee.ts │ │ │ │ ├── Media.ts │ │ │ │ ├── Seasons.ts │ │ │ │ └── Shop.ts │ │ ├── index.ts │ │ └── templates │ │ │ └── Standard.ts │ ├── shared │ │ ├── favicon │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-96x96.png │ │ │ ├── favicon.ico │ │ │ ├── favicon.svg │ │ │ ├── site.webmanifest │ │ │ ├── web-app-manifest-192x192.png │ │ │ └── web-app-manifest-512x512.png │ │ ├── fonts │ │ │ ├── editorial-new.woff │ │ │ ├── editorial-new.woff2 │ │ │ ├── neue-montreal.woff │ │ │ └── neue-montreal.woff2 │ │ ├── open-graph.png │ │ └── robots.txt │ ├── sprites │ │ ├── instagram.svg │ │ └── twitter.svg │ └── styles │ │ ├── base │ │ ├── fonts.scss │ │ └── reset.scss │ │ ├── components │ │ ├── bag.scss │ │ ├── footer.scss │ │ ├── menu.scss │ │ └── navigation.scss │ │ ├── index.scss │ │ ├── pages │ │ └── page.scss │ │ ├── sections │ │ ├── categories.scss │ │ ├── columns.scss │ │ ├── contact.scss │ │ ├── details.scss │ │ ├── disclaimer.scss │ │ ├── error.scss │ │ ├── gallery.scss │ │ ├── header.scss │ │ ├── hero.scss │ │ ├── highlight.scss │ │ ├── information.scss │ │ ├── ingredients.scss │ │ ├── intro.scss │ │ ├── list.scss │ │ ├── lookbook.scss │ │ ├── marquee.scss │ │ ├── media.scss │ │ ├── quote.scss │ │ ├── seasons.scss │ │ └── shop.scss │ │ ├── shared │ │ ├── animations.scss │ │ ├── button.scss │ │ ├── links.scss │ │ └── sections.scss │ │ └── utils │ │ └── variables.scss │ ├── stats.html │ ├── stylelint.config.js │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── turbo.json │ ├── utilities │ └── data.ts │ ├── vercel.json │ └── views │ ├── base.twig │ ├── components │ └── button.twig │ ├── layout │ ├── footer.twig │ ├── menu.twig │ └── navigation.twig │ ├── pages │ └── page.twig │ ├── sections │ ├── _categories.twig │ ├── _columns.twig │ ├── _contact.twig │ ├── _details.twig │ ├── _disclaimer.twig │ ├── _error.twig │ ├── _gallery.twig │ ├── _header.twig │ ├── _hero.twig │ ├── _highlight.twig │ ├── _information.twig │ ├── _ingredients.twig │ ├── _intro.twig │ ├── _list.twig │ ├── _lookbook.twig │ ├── _marquee.twig │ ├── _media.twig │ ├── _quote.twig │ ├── _seasons.twig │ ├── _shop.twig │ └── index.twig │ └── shared │ ├── header.twig │ └── scripts.twig ├── package-lock.json ├── package.json ├── packages ├── cli │ ├── .eslintrc.js │ ├── index.js │ └── package.json ├── config-eslint │ ├── .eslintrc.js │ ├── index.js │ └── package.json ├── config-stylelint │ ├── index.js │ └── package.json ├── config-tsconfig │ ├── base.json │ └── package.json ├── core │ ├── package.json │ └── src │ │ ├── App.ts │ │ ├── Component.ts │ │ ├── EventEmitter.ts │ │ ├── Link.ts │ │ ├── Links.ts │ │ ├── Page.ts │ │ └── index.ts ├── managers │ ├── package.json │ └── src │ │ ├── Pointer.ts │ │ ├── Viewport.ts │ │ └── index.ts ├── styles │ ├── package.json │ ├── scss │ │ ├── base │ │ │ ├── app.scss │ │ │ ├── lenis.scss │ │ │ └── reset.scss │ │ ├── index.scss │ │ ├── plugins │ │ │ └── include-media.scss │ │ ├── utils │ │ │ ├── functions.scss │ │ │ └── mixins.scss │ │ └── variables │ │ │ └── easings.scss │ └── stylelint.config.js └── utilities │ ├── package.json │ └── src │ ├── Canvas.ts │ ├── DOM.ts │ ├── Detection.ts │ ├── Math.ts │ ├── Polyfill.ts │ ├── Promises.ts │ ├── Text.ts │ └── index.ts └── turbo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.twig] 12 | indent_size = 4 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | # Debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # Misc 36 | .DS_Store 37 | *.pem 38 | 39 | # Rollup 40 | .rollup.cache 41 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "useTabs": false, 8 | "overrides": [ 9 | { 10 | "files": ["*.twig"], 11 | "options": { 12 | "parser": "twig", 13 | "printWidth": 120, 14 | "singleQuote": false, 15 | "tabWidth": 4, 16 | "useTabs": false 17 | } 18 | } 19 | ], 20 | "plugins": ["@zackad/prettier-plugin-twig"] 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lisergia 2 | 3 | [Luis Bizarro](https://bizar.ro/)'s opinative web development stack and Front End framework. 4 | 5 | ## Quick Start 6 | 7 | ```sh 8 | git clone git@github.com:bizarro/lisergia.git 9 | npm i 10 | npm run dev 11 | ``` 12 | 13 | ## Why this does exist? 14 | 15 | > “Prioritizing animations, motion and interactions in a website shouldn't be controversial. Not adding interesting things to your web pages because of metrics will always be a downgrade.” **— Luis Bizarro.** 16 | 17 | Lisergia is an opinionated web development stack designed to simplify the creation of interactive websites. Its primary goal is to reduce the complexity and stigma often associated with adding animations to web pages—a task that should be simple and straightforward. 18 | 19 | Everything in this repository is intentionally opinionated and reflects what I consider the golden standard for maintainability and scalability in web applications—specifically for marketing-focused landing pages. This stack is not intended for general product engineering, and that’s by design. Lisergia is solving a different problem. 20 | 21 | Marketing landing pages typically align with the creative vision of art directors and designers. These projects often prioritize delightful and surprising user experiences over conventional performance metrics. As such, benchmarks like LCP (Largest Contentful Paint) and FCP (First Contentful Paint) are less relevant. The real goal is to offer elegant transitions and engaging interactions that elevate the experience for site visitors. 22 | 23 | ## Front End Architecture 24 | 25 | The Front End architecture is primarily composed of [Twig](https://twig.symfony.com/) for HTML markup, [SCSS](https://sass-lang.com/) for styling and [TypeScript](https://www.typescriptlang.org/) for JavaScript transpilation. Lisergia also includes several libraries by default to streamline development and improve the developer experience: 26 | 27 | - [Lenis](https://lenis.darkroom.engineering/): improves scroll behavior to feel smooth and natural by default. 28 | - [MobX](https://mobx.js.org/): simplifies and streamlines application state management. 29 | - [NanoEvents](https://github.com/ai/nanoevents): enables lightweight event handling via `.on` and `.off` methods. 30 | - [Auto Bind](https://github.com/sindresorhus/auto-bind): eliminates the need for manual `.bind(this)` calls. 31 | - [Tempus](https://github.com/darkroomengineering/tempus): a `requestAnimationFrame` manager for coordinated frame-based updates. 32 | 33 | ## Back End Architecture 34 | 35 | The Back End uses [Sanity](https://www.sanity.io/) as the Content Management System, with [Express](https://expressjs.com/) serving a JSON file generated from its structured content. Express offers the simplicity and flexibility needed to create custom endpoints and integrate with third-party services such as [Resend](https://resend.com/). For rendering views, we use a Node.js port of [Twig](https://twig.symfony.com/) as the template engine. 36 | 37 | ## Deployment 38 | 39 | Just plug this on [Vercel](https://vercel.com/) and your application is going to be available automatically through [Vercel Functions](https://vercel.com/docs/functions). 40 | 41 | ## Apps 42 | 43 | - `cms`: A local Sanity CMS instance, running at `https://localhost:3333/` by default. 44 | - `web`: A local Express server, running at `https://localhost:3000/` by default. 45 | 46 | ## Packages 47 | 48 | - `@lisergia/config-eslint`: Shared `eslint` configuration used across the monorepo. 49 | - `@lisergia/config-typescript`: Shared `tsconfig.json` configurations used throughout the monorepo. 50 | - `@lisergia/cli`: Command-line interface used by the `web` application to generate the Front End bundle. 51 | - `@lisergia/core`: Core components that power the Lisergia framework. 52 | - `@lisergia/managers`: Built-in managers that handle various application behaviors in Lisergia. 53 | - `@lisergia/styles`: Shared styles and SCSS utilities used across projects. 54 | - `@lisergia/utilities`: Reusable utility functions that support common development needs. 55 | -------------------------------------------------------------------------------- /apps/cms/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@lisergia/config-eslint/index.js'], 4 | } 5 | -------------------------------------------------------------------------------- /apps/cms/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # Compiled Sanity Studio 9 | /dist 10 | 11 | # Temporary Sanity runtime, generated by the CLI on every dev server start 12 | /.sanity 13 | 14 | # Logs 15 | /logs 16 | *.log 17 | 18 | # Coverage directory used by testing tools 19 | /coverage 20 | 21 | # Misc 22 | .DS_Store 23 | *.pem 24 | 25 | # Typescript 26 | *.tsbuildinfo 27 | 28 | # Dotenv and similar local-only files 29 | *.local 30 | -------------------------------------------------------------------------------- /apps/cms/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lisergia/config-stylelint" 3 | } 4 | -------------------------------------------------------------------------------- /apps/cms/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Create a `.env` file with your project configuration: 4 | 5 | ``` 6 | SANITY_PROJECT=xxxxxxxx 7 | ``` 8 | 9 | ## Commands 10 | 11 | - `dev`: Run locally. 12 | - `start`: Preview static build. 13 | - `build`: Build into `dist` folder. 14 | - `deploy`: Deploy to `project.sanity.studio`. 15 | -------------------------------------------------------------------------------- /apps/cms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms", 3 | "private": true, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "dev": "sanity dev", 7 | "start": "sanity start", 8 | "build": "sanity build", 9 | "deploy": "sanity deploy", 10 | "deploy-graphql": "sanity graphql deploy" 11 | }, 12 | "dependencies": { 13 | "@lisergia/config-eslint": "*", 14 | "@lisergia/config-stylelint": "*", 15 | "@lisergia/config-tsconfig": "*", 16 | "@sanity/icons": "^3.7.0", 17 | "@sanity/vision": "^3.86.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "sanity": "^3.86.1", 21 | "styled-components": "^6.1.15" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.0.25", 25 | "eslint": "^9.9.0", 26 | "prettier": "^3.0.2", 27 | "typescript": "^5.1.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/cms/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import { defineCliConfig } from 'sanity/cli' 2 | 3 | export default defineCliConfig({ 4 | api: { 5 | projectId: process.env.SANITY_PROJECT, 6 | dataset: 'production', 7 | }, 8 | 9 | autoUpdates: true, 10 | }) 11 | -------------------------------------------------------------------------------- /apps/cms/sanity.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'sanity' 2 | import { structureTool } from 'sanity/structure' 3 | 4 | import { visionTool } from '@sanity/vision' 5 | 6 | import { schemaTypes } from './schemaTypes' 7 | import { structure } from './structure' 8 | 9 | export default defineConfig({ 10 | name: 'default', 11 | title: 'Lisergia', 12 | 13 | projectId: 'lqn1inyo', 14 | dataset: 'production', 15 | 16 | plugins: [structureTool({ structure }), visionTool()], 17 | 18 | schema: { 19 | types: schemaTypes, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/general/category.ts: -------------------------------------------------------------------------------- 1 | import { defineType, defineField } from 'sanity' 2 | 3 | export const category = defineType({ 4 | type: 'document', 5 | name: 'category', 6 | fields: [ 7 | defineField({ 8 | name: 'slug', 9 | options: { 10 | source: 'title', 11 | maxLength: 96, 12 | }, 13 | title: 'Slug', 14 | type: 'slug', 15 | validation: (Rule) => Rule.required(), 16 | }), 17 | 18 | defineField({ 19 | name: 'title', 20 | title: 'Title', 21 | type: 'string', 22 | validation: (Rule) => Rule.required(), 23 | }), 24 | ], 25 | }) 26 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/general/page.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const page = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'slug', 7 | options: { 8 | source: 'title', 9 | maxLength: 96, 10 | }, 11 | title: 'Slug', 12 | type: 'slug', 13 | }), 14 | 15 | defineField({ 16 | name: 'social', 17 | title: 'Social', 18 | type: 'social', 19 | }), 20 | 21 | defineField({ 22 | name: 'title', 23 | title: 'Title', 24 | type: 'string', 25 | }), 26 | 27 | defineField({ 28 | name: 'content', 29 | title: 'Content', 30 | type: 'content', 31 | }), 32 | ], 33 | name: 'page', 34 | title: 'Page', 35 | type: 'document', 36 | }) 37 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/general/product.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const product = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'slug', 7 | options: { 8 | source: 'title', 9 | maxLength: 96, 10 | }, 11 | title: 'Slug', 12 | type: 'slug', 13 | }), 14 | 15 | defineField({ 16 | name: 'social', 17 | title: 'Social', 18 | type: 'social', 19 | }), 20 | 21 | defineField({ 22 | name: 'image', 23 | title: 'Image', 24 | type: 'image', 25 | }), 26 | 27 | defineField({ 28 | name: 'label', 29 | title: 'Label', 30 | type: 'string', 31 | }), 32 | 33 | defineField({ 34 | name: 'title', 35 | title: 'Title', 36 | type: 'string', 37 | }), 38 | 39 | defineField({ 40 | name: 'price', 41 | title: 'Price', 42 | type: 'string', 43 | }), 44 | 45 | defineField({ 46 | name: 'content', 47 | title: 'Content', 48 | type: 'content', 49 | }), 50 | ], 51 | name: 'product', 52 | title: 'Product', 53 | type: 'document', 54 | }) 55 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/global/settings.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const settings = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | fields: [ 13 | defineField({ 14 | name: 'title', 15 | title: 'Title', 16 | type: 'string', 17 | }), 18 | 19 | defineField({ 20 | name: 'placeholder', 21 | title: 'Placeholder', 22 | type: 'string', 23 | }), 24 | 25 | defineField({ 26 | name: 'submit', 27 | title: 'Submit', 28 | type: 'string', 29 | }), 30 | ], 31 | name: 'newsletter', 32 | title: 'Newsletter', 33 | type: 'object', 34 | }), 35 | 36 | defineField({ 37 | fields: [ 38 | defineField({ 39 | name: 'title', 40 | title: 'Title', 41 | type: 'string', 42 | }), 43 | 44 | defineField({ 45 | name: 'list', 46 | of: [ 47 | defineArrayMember({ 48 | name: 'link', 49 | title: 'Link', 50 | type: 'link', 51 | }), 52 | ], 53 | type: 'array', 54 | }), 55 | ], 56 | name: 'social', 57 | title: 'Social', 58 | type: 'object', 59 | }), 60 | ], 61 | name: 'settings', 62 | title: 'Settings', 63 | type: 'document', 64 | }) 65 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/index.ts: -------------------------------------------------------------------------------- 1 | import { content } from './shared/content' 2 | import { link } from './shared/link' 3 | import { social } from './shared/social' 4 | 5 | import { footer } from './layout/footer' 6 | import { menu } from './layout/menu' 7 | 8 | import { category } from './general/category' 9 | import { page } from './general/page' 10 | import { product } from './general/product' 11 | 12 | import { settings } from './global/settings' 13 | 14 | import { categories } from './modules/categories' 15 | import { columns } from './modules/columns' 16 | import { details } from './modules/details' 17 | import { contact } from './modules/contact' 18 | import { disclaimer } from './modules/disclaimer' 19 | import { error } from './modules/error' 20 | import { gallery } from './modules/gallery' 21 | import { header } from './modules/header' 22 | import { hero } from './modules/hero' 23 | import { highlight } from './modules/highlight' 24 | import { information } from './modules/information' 25 | import { ingredients } from './modules/ingredients' 26 | import { intro } from './modules/intro' 27 | import { list } from './modules/list' 28 | import { lookbook } from './modules/lookbook' 29 | import { marquee } from './modules/marquee' 30 | import { media } from './modules/media' 31 | import { quote } from './modules/quote' 32 | import { seasons } from './modules/seasons' 33 | import { shop } from './modules/shop' 34 | 35 | export const schemaTypes = [ 36 | // Shared 37 | content, 38 | link, 39 | social, 40 | 41 | // Types 42 | category, 43 | page, 44 | product, 45 | 46 | // Modules 47 | categories, 48 | columns, 49 | contact, 50 | details, 51 | disclaimer, 52 | error, 53 | gallery, 54 | header, 55 | hero, 56 | highlight, 57 | information, 58 | ingredients, 59 | intro, 60 | list, 61 | lookbook, 62 | marquee, 63 | media, 64 | quote, 65 | seasons, 66 | shop, 67 | 68 | // Settings 69 | footer, 70 | menu, 71 | settings, 72 | ] 73 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/layout/footer.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const footer = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | name: 'list', 13 | of: [ 14 | defineArrayMember({ 15 | name: 'link', 16 | title: 'Link', 17 | type: 'link', 18 | }), 19 | ], 20 | type: 'array', 21 | }), 22 | 23 | defineField({ 24 | name: 'copyright', 25 | title: 'Copyright', 26 | type: 'string', 27 | }), 28 | 29 | defineField({ 30 | name: 'credits', 31 | of: [ 32 | { 33 | type: 'block', 34 | }, 35 | ], 36 | title: 'Credits', 37 | type: 'array', 38 | }), 39 | ], 40 | name: 'footer', 41 | title: 'Footer', 42 | type: 'document', 43 | }) 44 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/layout/menu.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const menu = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | name: 'list', 13 | of: [ 14 | defineArrayMember({ 15 | name: 'link', 16 | title: 'Link', 17 | type: 'link', 18 | }), 19 | ], 20 | type: 'array', 21 | }), 22 | 23 | defineField({ 24 | name: 'sublist', 25 | of: [ 26 | defineArrayMember({ 27 | name: 'link', 28 | title: 'Link', 29 | type: 'link', 30 | }), 31 | ], 32 | type: 'array', 33 | }), 34 | ], 35 | name: 'menu', 36 | title: 'Menu', 37 | type: 'document', 38 | }) 39 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/categories.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const categories = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'label', 7 | title: 'Label', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | name: 'title', 13 | title: 'Title', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'list', 19 | of: [ 20 | defineArrayMember({ 21 | fields: [ 22 | defineField({ 23 | name: 'image', 24 | title: 'Image', 25 | type: 'image', 26 | }), 27 | 28 | defineField({ 29 | name: 'link', 30 | title: 'Link', 31 | type: 'link', 32 | }), 33 | ], 34 | title: 'Entry', 35 | type: 'object', 36 | }), 37 | ], 38 | type: 'array', 39 | }), 40 | ], 41 | name: 'categories', 42 | title: 'Categories', 43 | type: 'object', 44 | }) 45 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/columns.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const columns = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'type', 7 | title: 'Type', 8 | type: 'string', 9 | options: { 10 | list: [ 11 | { value: 'left', title: 'Left' }, 12 | { value: 'right', title: 'Right' }, 13 | ], 14 | }, 15 | }), 16 | 17 | defineField({ 18 | name: 'label', 19 | title: 'Label', 20 | type: 'string', 21 | }), 22 | 23 | defineField({ 24 | name: 'title', 25 | title: 'Title', 26 | type: 'string', 27 | }), 28 | 29 | defineField({ 30 | name: 'description', 31 | title: 'Description', 32 | type: 'string', 33 | }), 34 | 35 | defineField({ 36 | name: 'list', 37 | of: [ 38 | defineArrayMember({ 39 | name: 'image', 40 | title: 'Image', 41 | type: 'image', 42 | }), 43 | ], 44 | type: 'array', 45 | }), 46 | ], 47 | name: 'columns', 48 | title: 'Columns', 49 | type: 'object', 50 | }) 51 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/contact.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const contact = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'list', 7 | of: [ 8 | defineArrayMember({ 9 | fields: [ 10 | defineField({ 11 | name: 'title', 12 | title: 'Title', 13 | type: 'string', 14 | }), 15 | defineField({ 16 | name: 'description', 17 | of: [ 18 | { 19 | type: 'block', 20 | }, 21 | ], 22 | title: 'Description', 23 | type: 'array', 24 | }), 25 | ], 26 | name: 'entry', 27 | title: 'Entry', 28 | type: 'object', 29 | }), 30 | ], 31 | type: 'array', 32 | }), 33 | ], 34 | name: 'contact', 35 | title: 'Contact', 36 | type: 'object', 37 | }) 38 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/details.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const details = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'description', 7 | of: [ 8 | { 9 | type: 'block', 10 | }, 11 | ], 12 | title: 'Description', 13 | type: 'array', 14 | }), 15 | 16 | defineField({ 17 | name: 'gallery', 18 | of: [ 19 | defineArrayMember({ 20 | name: 'image', 21 | title: 'Image', 22 | type: 'image', 23 | }), 24 | ], 25 | type: 'array', 26 | }), 27 | ], 28 | name: 'details', 29 | title: 'Details', 30 | type: 'object', 31 | }) 32 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/disclaimer.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const disclaimer = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'text', 9 | }), 10 | 11 | defineField({ 12 | name: 'author', 13 | title: 'Author', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'description', 19 | of: [ 20 | { 21 | type: 'block', 22 | }, 23 | ], 24 | title: 'Description', 25 | type: 'array', 26 | }), 27 | ], 28 | name: 'disclaimer', 29 | title: 'Disclaimer', 30 | type: 'object', 31 | }) 32 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/error.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const error = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'image', 7 | title: 'Image', 8 | type: 'image', 9 | }), 10 | 11 | defineField({ 12 | name: 'title', 13 | title: 'Title', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'description', 19 | title: 'Description', 20 | type: 'string', 21 | }), 22 | 23 | defineField({ 24 | name: 'button', 25 | title: 'Button', 26 | type: 'link', 27 | }), 28 | ], 29 | name: 'error', 30 | title: 'Error', 31 | type: 'object', 32 | }) 33 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/gallery.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const gallery = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | name: 'list', 13 | of: [ 14 | defineArrayMember({ 15 | name: 'image', 16 | title: 'Image', 17 | type: 'image', 18 | }), 19 | ], 20 | type: 'array', 21 | }), 22 | ], 23 | name: 'gallery', 24 | title: 'Gallery', 25 | type: 'object', 26 | }) 27 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/header.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const header = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'image', 7 | title: 'Image', 8 | type: 'image', 9 | }), 10 | 11 | defineField({ 12 | name: 'title', 13 | title: 'Title', 14 | type: 'string', 15 | }), 16 | ], 17 | name: 'header', 18 | title: 'Header', 19 | type: 'object', 20 | }) 21 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/hero.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const hero = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'image', 7 | title: 'Image', 8 | type: 'image', 9 | }), 10 | 11 | defineField({ 12 | name: 'title', 13 | title: 'Title', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'button', 19 | title: 'Button', 20 | type: 'link', 21 | }), 22 | ], 23 | name: 'hero', 24 | title: 'Hero', 25 | type: 'object', 26 | }) 27 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/highlight.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const highlight = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'label', 7 | title: 'Label', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | name: 'title', 13 | title: 'Title', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'description', 19 | title: 'Description', 20 | type: 'string', 21 | }), 22 | 23 | defineField({ 24 | name: 'list', 25 | of: [ 26 | defineArrayMember({ 27 | name: 'image', 28 | title: 'Image', 29 | type: 'image', 30 | }), 31 | ], 32 | type: 'array', 33 | }), 34 | ], 35 | name: 'highlight', 36 | title: 'Highlight', 37 | type: 'object', 38 | }) 39 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/information.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const information = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'type', 7 | title: 'Type', 8 | type: 'string', 9 | options: { 10 | list: [ 11 | { value: 'left', title: 'Left' }, 12 | { value: 'right', title: 'Right' }, 13 | ], 14 | }, 15 | }), 16 | 17 | defineField({ 18 | name: 'description', 19 | of: [ 20 | { 21 | type: 'block', 22 | }, 23 | ], 24 | title: 'Description', 25 | type: 'array', 26 | }), 27 | 28 | defineField({ 29 | name: 'image', 30 | title: 'Image', 31 | type: 'image', 32 | }), 33 | ], 34 | name: 'information', 35 | title: 'Information', 36 | type: 'object', 37 | }) 38 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/ingredients.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const ingredients = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'image', 7 | title: 'Image', 8 | type: 'image', 9 | }), 10 | 11 | defineField({ 12 | name: 'title', 13 | title: 'Title', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'description', 19 | of: [ 20 | { 21 | type: 'block', 22 | }, 23 | ], 24 | title: 'Description', 25 | type: 'array', 26 | }), 27 | 28 | defineField({ 29 | name: 'list', 30 | of: [ 31 | defineArrayMember({ 32 | fields: [ 33 | defineField({ 34 | name: 'title', 35 | title: 'Title', 36 | type: 'string', 37 | }), 38 | 39 | defineField({ 40 | name: 'region', 41 | title: 'Region', 42 | type: 'string', 43 | }), 44 | 45 | defineField({ 46 | name: 'ingredient', 47 | title: 'Ingredient', 48 | type: 'string', 49 | }), 50 | ], 51 | name: 'entry', 52 | title: 'Entry', 53 | type: 'object', 54 | }), 55 | ], 56 | type: 'array', 57 | }), 58 | ], 59 | name: 'ingredients', 60 | title: 'Ingredients', 61 | type: 'object', 62 | }) 63 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/intro.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const intro = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'image', 7 | title: 'Image', 8 | type: 'image', 9 | }), 10 | 11 | defineField({ 12 | name: 'label', 13 | title: 'Label', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'title', 19 | title: 'Title', 20 | type: 'string', 21 | }), 22 | 23 | defineField({ 24 | name: 'description', 25 | title: 'Description', 26 | type: 'string', 27 | }), 28 | ], 29 | name: 'intro', 30 | title: 'Intro', 31 | type: 'object', 32 | }) 33 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/list.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const list = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'label', 7 | title: 'Label', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | name: 'list', 13 | of: [ 14 | defineArrayMember({ 15 | name: 'link', 16 | title: 'Link', 17 | type: 'link', 18 | }), 19 | ], 20 | type: 'array', 21 | }), 22 | 23 | defineField({ 24 | name: 'button', 25 | title: 'Button', 26 | type: 'link', 27 | }), 28 | ], 29 | name: 'list', 30 | title: 'List', 31 | type: 'object', 32 | }) 33 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/lookbook.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const lookbook = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'list', 7 | of: [ 8 | defineArrayMember({ 9 | fields: [ 10 | defineField({ 11 | name: 'content', 12 | of: [ 13 | defineArrayMember({ 14 | fields: [ 15 | defineField({ 16 | fields: [ 17 | defineField({ 18 | name: 'type', 19 | title: 'Type', 20 | type: 'string', 21 | options: { 22 | list: [ 23 | { title: 'Image', value: 'image' }, 24 | { title: 'Text', value: 'text' }, 25 | ], 26 | }, 27 | }), 28 | 29 | defineField({ 30 | of: [ 31 | { 32 | type: 'block', 33 | }, 34 | ], 35 | name: 'title', 36 | title: 'Title', 37 | type: 'array', 38 | hidden: ({ parent }) => parent?.type !== 'text', 39 | }), 40 | 41 | defineField({ 42 | name: 'image', 43 | title: 'Image', 44 | type: 'image', 45 | hidden: ({ parent }) => parent?.type !== 'image', 46 | }), 47 | ], 48 | name: 'entry', 49 | title: 'Entry', 50 | type: 'object', 51 | }), 52 | ], 53 | name: 'item', 54 | title: 'Item', 55 | type: 'object', 56 | }), 57 | ], 58 | type: 'array', 59 | }), 60 | ], 61 | name: 'item', 62 | title: 'Item', 63 | type: 'object', 64 | }), 65 | ], 66 | type: 'array', 67 | }), 68 | ], 69 | name: 'lookbook', 70 | title: 'Lookbook', 71 | type: 'object', 72 | }) 73 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/marquee.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const marquee = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'string', 9 | }), 10 | ], 11 | name: 'marquee', 12 | title: 'Marquee', 13 | type: 'object', 14 | }) 15 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/media.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const media = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'media', 7 | title: 'Media', 8 | type: 'file', 9 | }), 10 | ], 11 | name: 'media', 12 | title: 'Media', 13 | type: 'object', 14 | }) 15 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/quote.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const quote = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'text', 9 | }), 10 | 11 | defineField({ 12 | name: 'list', 13 | of: [ 14 | defineArrayMember({ 15 | name: 'image', 16 | title: 'Image', 17 | type: 'image', 18 | }), 19 | ], 20 | type: 'array', 21 | }), 22 | ], 23 | name: 'quote', 24 | title: 'Quote', 25 | type: 'object', 26 | }) 27 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/seasons.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const seasons = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'title', 7 | title: 'Title', 8 | type: 'string', 9 | }), 10 | 11 | defineField({ 12 | name: 'description', 13 | title: 'Description', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'label', 19 | title: 'Label', 20 | type: 'string', 21 | }), 22 | 23 | defineField({ 24 | name: 'list', 25 | of: [ 26 | defineArrayMember({ 27 | name: 'image', 28 | title: 'Image', 29 | type: 'image', 30 | }), 31 | ], 32 | type: 'array', 33 | }), 34 | ], 35 | name: 'seasons', 36 | title: 'Seasons', 37 | type: 'object', 38 | }) 39 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/modules/shop.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineField, defineType } from 'sanity' 2 | 3 | export const shop = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'list', 7 | of: [ 8 | defineArrayMember({ 9 | fields: [ 10 | defineField({ 11 | name: 'category', 12 | title: 'Category', 13 | type: 'reference', 14 | to: [{ type: 'category' }], 15 | }), 16 | 17 | defineField({ 18 | name: 'content', 19 | of: [ 20 | defineArrayMember({ 21 | fields: [ 22 | defineField({ 23 | fields: [ 24 | defineField({ 25 | name: 'type', 26 | title: 'Type', 27 | type: 'string', 28 | options: { 29 | list: [ 30 | { title: 'Image', value: 'image' }, 31 | { title: 'Product', value: 'product' }, 32 | ], 33 | }, 34 | }), 35 | 36 | defineField({ 37 | name: 'product', 38 | title: 'Product', 39 | to: [{ type: 'product' }], 40 | type: 'reference', 41 | hidden: ({ parent }) => parent?.type !== 'product', 42 | }), 43 | 44 | defineField({ 45 | name: 'image', 46 | title: 'Image', 47 | type: 'image', 48 | hidden: ({ parent }) => parent?.type !== 'image', 49 | }), 50 | ], 51 | name: 'entry', 52 | title: 'Entry', 53 | type: 'object', 54 | }), 55 | ], 56 | name: 'item', 57 | title: 'Item', 58 | type: 'object', 59 | }), 60 | ], 61 | type: 'array', 62 | }), 63 | ], 64 | name: 'item', 65 | title: 'Item', 66 | type: 'object', 67 | }), 68 | ], 69 | type: 'array', 70 | }), 71 | ], 72 | name: 'shop', 73 | title: 'Shop', 74 | type: 'object', 75 | }) 76 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/shared/content.ts: -------------------------------------------------------------------------------- 1 | import { defineArrayMember, defineType } from 'sanity' 2 | 3 | export const content = defineType({ 4 | name: 'content', 5 | of: [ 6 | defineArrayMember({ 7 | name: 'categories', 8 | type: 'categories', 9 | }), 10 | defineArrayMember({ 11 | name: 'columns', 12 | type: 'columns', 13 | }), 14 | defineArrayMember({ 15 | name: 'contact', 16 | type: 'contact', 17 | }), 18 | defineArrayMember({ 19 | name: 'details', 20 | type: 'details', 21 | }), 22 | defineArrayMember({ 23 | name: 'disclaimer', 24 | type: 'disclaimer', 25 | }), 26 | defineArrayMember({ 27 | name: 'error', 28 | type: 'error', 29 | }), 30 | defineArrayMember({ 31 | name: 'gallery', 32 | type: 'gallery', 33 | }), 34 | defineArrayMember({ 35 | name: 'header', 36 | type: 'header', 37 | }), 38 | defineArrayMember({ 39 | name: 'hero', 40 | type: 'hero', 41 | }), 42 | defineArrayMember({ 43 | name: 'highlight', 44 | type: 'highlight', 45 | }), 46 | defineArrayMember({ 47 | name: 'information', 48 | type: 'information', 49 | }), 50 | defineArrayMember({ 51 | name: 'ingredients', 52 | type: 'ingredients', 53 | }), 54 | defineArrayMember({ 55 | name: 'intro', 56 | type: 'intro', 57 | }), 58 | defineArrayMember({ 59 | name: 'list', 60 | type: 'list', 61 | }), 62 | defineArrayMember({ 63 | name: 'lookbook', 64 | type: 'lookbook', 65 | }), 66 | defineArrayMember({ 67 | name: 'marquee', 68 | type: 'marquee', 69 | }), 70 | defineArrayMember({ 71 | name: 'media', 72 | type: 'media', 73 | }), 74 | defineArrayMember({ 75 | name: 'quote', 76 | type: 'quote', 77 | }), 78 | defineArrayMember({ 79 | name: 'seasons', 80 | type: 'seasons', 81 | }), 82 | defineArrayMember({ 83 | name: 'shop', 84 | type: 'shop', 85 | }), 86 | ], 87 | title: 'Content', 88 | type: 'array', 89 | }) 90 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/shared/link.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const link = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'text', 7 | type: 'string', 8 | }), 9 | 10 | defineField({ 11 | name: 'url', 12 | type: 'url', 13 | title: 'URL', 14 | validation: (Rule) => 15 | Rule.uri({ 16 | allowRelative: true, 17 | scheme: ['http', 'https', 'mailto'], 18 | }), 19 | }), 20 | ], 21 | name: 'link', 22 | title: 'Link', 23 | type: 'object', 24 | }) 25 | -------------------------------------------------------------------------------- /apps/cms/schemaTypes/shared/social.ts: -------------------------------------------------------------------------------- 1 | import { defineField, defineType } from 'sanity' 2 | 3 | export const social = defineType({ 4 | fields: [ 5 | defineField({ 6 | name: 'image', 7 | title: 'Image', 8 | type: 'image', 9 | }), 10 | 11 | defineField({ 12 | name: 'title', 13 | title: 'Title', 14 | type: 'string', 15 | }), 16 | 17 | defineField({ 18 | name: 'description', 19 | title: 'Description', 20 | type: 'string', 21 | }), 22 | ], 23 | name: 'social', 24 | title: 'Social', 25 | type: 'object', 26 | }) 27 | -------------------------------------------------------------------------------- /apps/cms/structure/index.ts: -------------------------------------------------------------------------------- 1 | import type { StructureResolver } from 'sanity/structure' 2 | 3 | import { CogIcon, DocumentsIcon, TiersIcon } from '@sanity/icons' 4 | 5 | export const structure: StructureResolver = (S, context) => 6 | S.list() 7 | .id('root') 8 | .title('Lisergia') 9 | .items([ 10 | S.documentTypeListItem('category').title('Categories').icon(DocumentsIcon), 11 | S.documentTypeListItem('product').title('Products').icon(DocumentsIcon), 12 | S.documentTypeListItem('page').title('Pages').icon(TiersIcon), 13 | 14 | S.listItem().title('Menu').id('menu').child(S.document().schemaType('menu').documentId('menu')).icon(CogIcon), 15 | 16 | S.listItem() 17 | .title('Footer') 18 | .id('footer') 19 | .child(S.document().schemaType('footer').documentId('footer')) 20 | .icon(CogIcon), 21 | 22 | S.listItem() 23 | .title('Settings') 24 | .id('settings') 25 | .child(S.document().schemaType('settings').documentId('settings')) 26 | .icon(CogIcon), 27 | ]) 28 | -------------------------------------------------------------------------------- /apps/cms/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lisergia/config-tsconfig/base.json", 3 | "compilerOptions": { 4 | "target": "ES2017", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "module": "Preserve", 7 | "jsx": "preserve", 8 | "incremental": true 9 | }, 10 | "include": ["**/*.ts", "**/*.tsx"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@lisergia/config-eslint/index.js'], 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # JSON Content 2 | content.json 3 | 4 | # Output 5 | .vercel 6 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Create a `.env` file with your project configuration: 4 | 5 | ```env 6 | GOOGLE_ANALYTICS=ABC 7 | 8 | SANITY_API=2023-05-03 9 | SANITY_DATABASE=production 10 | SANITY_PROJECT=xxxxxxxx 11 | 12 | BROWSERSYNC_PORT=3000 13 | BROWSERSYNC_PROXY=localhost:3000 14 | 15 | TYPEKIT=xxxxxxx 16 | 17 | KLAVIYO_API_KEY=xx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 18 | KLAVIYO_COMPANY_ID=xxxxxx 19 | KLAVIYO_LIST_ID=xxxxxx 20 | ``` 21 | 22 | ## Environment Variables 23 | 24 | - `GOOGLE_ANALYTICS`: Injects Google Analytics tracking when set. 25 | - `SANITY_API`: Defines the Sanity API version. 26 | - `SANITY_DATABASE`: Specifies the Sanity dataset to use. 27 | - `SANITY_PROJECT`: Sets the Sanity project ID. 28 | - `BROWSERSYNC_PORT`: Overrides the default Browsersync port. 29 | - `BROWSERSYNC_PROXY`: Overrides the default Browsersync proxy target. 30 | - `TYPEKIT`: Injects Adobe Typekit fonts into the site. 31 | - `KLAVIYO_API_KEY`: API key used to connect with Klaviyo services. 32 | - `KLAVIYO_COMPANY_ID`: Identifier for your Klaviyo company account. 33 | - `KLAVIYO_LIST_ID`: Identifier for the Klaviyo mailing list used in forms or signups. 34 | 35 | ## Commands 36 | 37 | - `content`: Generates the content.json file from the CMS data. 38 | - `dev`: Starts the development server with Rollup, Express, and Nodemon. 39 | - `build`: Compiles the Express index.js application and static assets. 40 | - `lint`: Lints the codebase using ESLint. 41 | - `start`: Serves the production build locally for testing. 42 | -------------------------------------------------------------------------------- /apps/web/client.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | import { createClient } from '@sanity/client' 4 | 5 | const client = createClient({ 6 | apiVersion: process.env.SANITY_API, 7 | dataset: process.env.SANITY_DATABASE, 8 | projectId: process.env.SANITY_PROJECT, 9 | useCdn: true, 10 | }) 11 | 12 | export { client } 13 | -------------------------------------------------------------------------------- /apps/web/controllers/newsletter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | 3 | export default async (request: Request, response: Response) => { 4 | const { email } = request.body 5 | 6 | try { 7 | const body = JSON.stringify({ 8 | data: { 9 | type: 'profile-subscription-bulk-create-job', 10 | attributes: { 11 | profiles: { 12 | data: [ 13 | { 14 | type: 'profile', 15 | attributes: { 16 | subscriptions: { 17 | email: { 18 | marketing: { 19 | consent: 'SUBSCRIBED', 20 | }, 21 | }, 22 | }, 23 | email, 24 | }, 25 | }, 26 | ], 27 | }, 28 | historical_import: false, 29 | }, 30 | relationships: { 31 | list: { 32 | data: { 33 | id: process.env.KLAVIYO_LIST_ID, 34 | type: 'list', 35 | }, 36 | }, 37 | }, 38 | }, 39 | }) 40 | 41 | const url = 'https://a.klaviyo.com/api/profile-subscription-bulk-create-jobs' 42 | const options = { 43 | method: 'POST', 44 | headers: { 45 | accept: 'application/vnd.api+json', 46 | revision: '2025-04-15', 47 | 'content-type': 'application/vnd.api+json', 48 | Authorization: `Klaviyo-API-Key ${process.env.KLAVIYO_API_KEY}`, 49 | }, 50 | body, 51 | } 52 | 53 | const fetchResponse = await fetch(url, options) 54 | 55 | if (fetchResponse.status === 202) { 56 | response.status(202).send('Successfully subscribed!') 57 | } else { 58 | response.status(fetchResponse.status).send('Subscription failed.') 59 | } 60 | } catch (error) { 61 | console.error('Error Subscribing:', error) 62 | 63 | response.status(500).send('An error occurred.') 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apps/web/controllers/page.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | 3 | import { getData } from '../utilities/data' 4 | 5 | export default async (request: Request, response: Response) => { 6 | response.render('pages/page', { 7 | ...getData(request), 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/download.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import isArray from 'lodash/isArray' 3 | import isObject from 'lodash/isObject' 4 | 5 | import * as AssetUtils from '@sanity/asset-utils' 6 | 7 | import { client } from './client.js' 8 | 9 | const [footer] = await client.fetch(`*[_type == "footer"]`) 10 | const [menu] = await client.fetch(`*[_type == "menu"]`) 11 | const [settings] = await client.fetch(`*[_type == "settings"]`) 12 | 13 | const categories = await client.fetch(`*[_type == "category"]`) 14 | const pages = await client.fetch(`*[_type == "page"]`) 15 | const products = await client.fetch(`*[_type == "product"]`) 16 | 17 | const references = [...categories, ...products] 18 | 19 | const traverse = (object: any, callback: Function) => { 20 | for (const property in object) { 21 | const value = object[property] 22 | 23 | callback(object, property, value) 24 | 25 | if (isArray(value)) { 26 | value.forEach((value) => { 27 | traverse(value, callback) 28 | }) 29 | } 30 | 31 | if (isObject(value)) { 32 | traverse(value, callback) 33 | } 34 | } 35 | } 36 | 37 | const parsePage = (page: any) => { 38 | const map = new Map() 39 | 40 | traverse(page, (object: any, key: string, value: any) => { 41 | if (key === '_ref') { 42 | if (value.includes('image-')) { 43 | return 44 | } 45 | 46 | const reference = references.find((document) => document._id === value) 47 | 48 | if (reference) { 49 | map.set(object, reference) 50 | } 51 | } 52 | }) 53 | 54 | Array.from(map.entries()).forEach(([entry, reference]) => { 55 | Object.assign(entry, { ...reference }) 56 | }) 57 | } 58 | 59 | const allPages = [...categories, ...products, ...pages] 60 | 61 | allPages.forEach((page) => parsePage(page)) 62 | 63 | const body = { 64 | categories, 65 | footer, 66 | menu, 67 | pages, 68 | products, 69 | settings, 70 | } 71 | 72 | fs.writeFile('content.json', JSON.stringify(body, null, 4), 'utf8', () => { 73 | console.log('Content generated under content.json.') 74 | }) 75 | -------------------------------------------------------------------------------- /apps/web/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | import express from 'express' 4 | import path from 'path' 5 | import staticify from 'staticify' 6 | import { fileURLToPath } from 'url' 7 | 8 | import { getFileAsset, getImageAsset, SanityFileSource, SanityImageSource } from '@sanity/asset-utils' 9 | import { toHTML } from '@portabletext/to-html' 10 | 11 | import { client } from './client' 12 | import { router } from './router' 13 | 14 | const __filename = fileURLToPath(import.meta.url) 15 | const __dirname = path.dirname(__filename) 16 | 17 | const app = express() 18 | 19 | app.use(express.json()) 20 | app.use(express.urlencoded()) 21 | 22 | const staticifyInstance = staticify(path.join(__dirname, 'build')) 23 | 24 | app.use(staticifyInstance.middleware) 25 | 26 | app.set('views', path.join(__dirname, 'views')) 27 | app.set('view engine', 'twig') 28 | 29 | app.use((request, response, next) => { 30 | // Assets Configuration 31 | response.locals.getVersionedPath = staticifyInstance.getVersionedPath 32 | 33 | // Sanity Configuration 34 | const config = client.config() 35 | 36 | response.locals.getAsset = (asset: SanityImageSource) => { 37 | return getImageAsset(asset, { 38 | baseUrl: config.apiHost, 39 | projectId: config.projectId!, 40 | dataset: config.dataset!, 41 | }) 42 | } 43 | 44 | response.locals.getFile = (asset: SanityFileSource) => { 45 | return getFileAsset(asset, config) 46 | } 47 | 48 | response.locals.parseHTML = (blocks: any) => { 49 | if (blocks) { 50 | return toHTML(blocks) 51 | } 52 | } 53 | 54 | // Utilities 55 | response.locals.lowercase = (string: string) => { 56 | return string.toLowerCase() 57 | } 58 | 59 | next() 60 | }) 61 | 62 | app.use('/', router) 63 | 64 | const PORT = process.env.PORT || 3000 65 | 66 | app.listen(PORT, () => { 67 | console.log(`App listening on port ${PORT}`) 68 | }) 69 | 70 | export default app 71 | -------------------------------------------------------------------------------- /apps/web/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["**/*"], 3 | "ext": "js,json,scss,ts,tsx,twig", 4 | "ignore": ["node_modules"], 5 | "exec": "tsx index.ts" 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "content": "tsx download.ts", 8 | "dev": "concurrently --kill-others \"npm run dev:client\" \"npm run dev:server\"", 9 | "dev:server": "nodemon", 10 | "dev:client": "lisergia dev", 11 | "build": "npm run build:server && npm run build:client", 12 | "build:server": "tsup index.ts", 13 | "build:client": "npm run content && lisergia build", 14 | "postinstall": "npm run build:client", 15 | "lint": "stylelint \"**/*.scss\"", 16 | "start": "npm run build && tsx index.ts" 17 | }, 18 | "type": "module", 19 | "dependencies": { 20 | "@portabletext/to-html": "^2.0.14", 21 | "@sanity/asset-utils": "^2.2.1", 22 | "@sanity/client": "^6.29.1", 23 | "animejs": "^4.0.2", 24 | "dotenv": "^16.5.0", 25 | "eslint": "^9.9.0", 26 | "express": "^5.1.0", 27 | "nodemon": "^3.1.10", 28 | "staticify": "^5.0.0", 29 | "stylelint": "^16.19.1", 30 | "tsup": "^8.4.0", 31 | "tsx": "^4.19.4", 32 | "twig": "^1.17.1", 33 | "ua-parser-js": "^2.0.3" 34 | }, 35 | "devDependencies": { 36 | "@lisergia/cli": "*", 37 | "@lisergia/config-eslint": "*", 38 | "@lisergia/config-stylelint": "*", 39 | "@lisergia/config-tsconfig": "*", 40 | "@lisergia/core": "*", 41 | "@lisergia/managers": "*", 42 | "@lisergia/styles": "*", 43 | "@lisergia/utilities": "*", 44 | "@types/express": "^5.0.1", 45 | "@types/express-serve-static-core": "^5.0.6", 46 | "@types/node": "^22.15.3", 47 | "concurrently": "^9.1.2", 48 | "eslint-config-prettier": "^8.6.0", 49 | "mobx": "^6.13.7", 50 | "prettier": "^3.0.2", 51 | "typescript": "^5.1.6" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/web/router/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import newsletter from '../controllers/newsletter' 4 | import page from '../controllers/page' 5 | 6 | import { getData } from '../utilities/data' 7 | 8 | const router = express.Router() 9 | 10 | router.get('/', page) 11 | router.get('/:slug', page) 12 | router.get('/product/:slug', page) 13 | 14 | router.post('/signup', newsletter) 15 | 16 | router.use((request, response) => { 17 | const data = getData(request) 18 | 19 | response.status(404) 20 | 21 | response.render('pages/page', { ...data }) 22 | }) 23 | 24 | export { router } 25 | -------------------------------------------------------------------------------- /apps/web/src/app/classes/Animation.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSelector, ComponentSelectors } from '@lisergia/core' 2 | 3 | export default class extends Component { 4 | declare delay: number 5 | declare elements: { 6 | target: HTMLElement 7 | } 8 | 9 | isVisible: boolean = false 10 | 11 | constructor({ element, elements }: { element: ComponentSelector; elements: ComponentSelectors }) { 12 | element = element as HTMLElement 13 | 14 | const { animationDelay, animationTarget } = element.dataset 15 | 16 | super({ 17 | element, 18 | elements: { 19 | ...elements, 20 | target: animationTarget ? element.closest(animationTarget)! : element, 21 | }, 22 | }) 23 | 24 | this.delay = parseInt(animationDelay ?? '0') 25 | } 26 | 27 | declare observer: IntersectionObserver 28 | 29 | createObserver() { 30 | this.observer = new IntersectionObserver((entries) => { 31 | entries.forEach((entry) => { 32 | if (!this.isVisible && entry.isIntersecting) { 33 | this.animateIn() 34 | } else { 35 | this.animateOut() 36 | } 37 | }) 38 | }) 39 | 40 | this.observer.observe(this.elements.target) 41 | } 42 | 43 | animateIn() { 44 | this.isVisible = true 45 | } 46 | 47 | animateOut() { 48 | this.isVisible = false 49 | } 50 | 51 | addEventListeners() { 52 | this.createObserver() 53 | } 54 | 55 | removeEventListeners() { 56 | this.observer.unobserve(this.elements.target) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/web/src/app/components/Menu.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@lisergia/core' 2 | 3 | export default class Menu extends Component {} 4 | -------------------------------------------------------------------------------- /apps/web/src/app/components/Navigation.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { autorun } from 'mobx' 3 | 4 | export default class Navigation extends Component { 5 | declare classes: { 6 | active: string 7 | open: string 8 | menuLinksActive: string 9 | } 10 | 11 | declare element: HTMLElement 12 | declare elements: { 13 | button: HTMLElement 14 | menu: HTMLElement 15 | menuLinks: NodeListOf 16 | } 17 | 18 | constructor({ application }: { application: ApplicationManager }) { 19 | super({ 20 | application, 21 | classes: { 22 | active: 'navigation--active', 23 | open: 'navigation--open', 24 | menuLinksActive: 'menu__list__link--active', 25 | }, 26 | element: '.navigation', 27 | elements: { 28 | button: '.navigation__button', 29 | 30 | menu: '.menu', 31 | menuLinks: '.menu__list__link', 32 | }, 33 | }) 34 | 35 | autorun(this.onChange) 36 | } 37 | 38 | onToggle() { 39 | if (document.documentElement.classList.contains(this.classes.open)) { 40 | document.documentElement.classList.remove(this.classes.open) 41 | } else { 42 | document.documentElement.classList.add(this.classes.open) 43 | } 44 | } 45 | 46 | onChange() { 47 | document.documentElement.classList.remove(this.classes.open) 48 | 49 | this.elements.menuLinks.forEach((link) => { 50 | if (this.application!.route.indexOf(link.href) > -1) { 51 | link.classList.add(this.classes.menuLinksActive) 52 | } else { 53 | link.classList.remove(this.classes.menuLinksActive) 54 | } 55 | }) 56 | } 57 | 58 | addEventListeners() { 59 | this.elements.button.addEventListener('click', this.onToggle, { passive: true }) 60 | } 61 | 62 | removeEventListeners(): void { 63 | this.elements.button.removeEventListener('click', this.onToggle) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apps/web/src/app/components/Transition.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | 3 | import { animate } from 'animejs' 4 | 5 | export default class Transition extends Component { 6 | constructor({ application }: { application: ApplicationManager }) { 7 | super({ 8 | application, 9 | id: 'transition', 10 | }) 11 | } 12 | 13 | onTransition(application: ApplicationManager) { 14 | const animation = animate(application.currentPage!.element, { 15 | duration: 1000, 16 | opacity: 0, 17 | }) 18 | 19 | animation.then(() => { 20 | application.currentPage!.element.remove() 21 | application.currentPage!.destroy() 22 | 23 | application.element.appendChild(application.nextPage.element!.firstElementChild!) 24 | 25 | application.createPage(application.nextPage.template) 26 | 27 | animate(application.currentPage!.element, { 28 | duration: 1000, 29 | opacity: { 30 | from: 0, 31 | to: 1, 32 | }, 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/Newsletter.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | 3 | export default class Newsletter extends Component { 4 | declare classes: { 5 | error: string 6 | success: string 7 | } 8 | 9 | declare element: HTMLElement 10 | declare elements: { 11 | form: HTMLFormElement 12 | } 13 | 14 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 15 | super({ 16 | application, 17 | classes: { 18 | error: 'footer__newsletter--error', 19 | success: 'footer__newsletter--success', 20 | }, 21 | element, 22 | elements: { 23 | form: 'form', 24 | }, 25 | }) 26 | } 27 | 28 | async onSubmit(event: Event) { 29 | event.preventDefault() 30 | 31 | const formData = new FormData(this.elements.form) 32 | const email = formData.get('email') 33 | 34 | const response = await window.fetch('/signup', { 35 | method: 'POST', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | body: JSON.stringify({ 40 | email, 41 | }), 42 | }) 43 | 44 | this.elements.form.setAttribute('disabled', 'disabled') 45 | 46 | if (response.ok) { 47 | this.element.classList.add(this.classes.success) 48 | } else { 49 | this.element.classList.add(this.classes.error) 50 | } 51 | } 52 | 53 | addEventListeners() { 54 | this.elements.form.addEventListener('submit', this.onSubmit) 55 | } 56 | 57 | removeEventListeners() { 58 | this.elements.form.removeEventListener('submit', this.onSubmit) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/Paragraph.ts: -------------------------------------------------------------------------------- 1 | import { SplitText } from '@lisergia/utilities' 2 | 3 | import Animation from '../classes/Animation' 4 | 5 | export default class extends Animation { 6 | declare element: HTMLElement 7 | declare elements: { 8 | paragraphs: NodeListOf 9 | lines: NodeListOf 10 | target: HTMLElement 11 | } 12 | 13 | constructor({ element }: { element: HTMLElement }) { 14 | const paragraphs = element.querySelectorAll('h1, h2, h3, h4, h5, h6, li, p') 15 | 16 | if (paragraphs.length) { 17 | paragraphs.forEach((element) => { 18 | SplitText.create(element, { 19 | type: 'lines', 20 | }) 21 | 22 | SplitText.create(element, { 23 | type: 'lines', 24 | }) 25 | }) 26 | } else { 27 | SplitText.create(element, { 28 | type: 'lines', 29 | }) 30 | 31 | SplitText.create(element, { 32 | type: 'lines', 33 | }) 34 | } 35 | 36 | super({ 37 | element, 38 | elements: { 39 | lines: element.querySelectorAll('div div'), 40 | }, 41 | }) 42 | } 43 | 44 | animateIn() { 45 | super.animateIn() 46 | 47 | this.elements.lines.forEach((element, lineIndex) => { 48 | element.style.transform = 'translateY(0) rotate(0)' 49 | element.style.transition = `transform 1.5s ${0.1 + lineIndex * 0.1}s var(--ease-out-expo)` 50 | }) 51 | } 52 | 53 | animateOut() { 54 | super.animateOut() 55 | 56 | let rotation = 0 57 | 58 | this.elements.lines.forEach((element, lineIndex) => { 59 | rotation += 0.15 60 | 61 | element.style.transform = `translateY(150%) rotate(${rotation}deg)` 62 | element.style.transition = '' 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/Parallax.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class Parallax extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | media: HTMLElement 11 | } 12 | 13 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 14 | super({ 15 | application, 16 | element, 17 | elements: { 18 | media: element.firstElementChild as HTMLElement, 19 | }, 20 | }) 21 | 22 | makeObservable(this, { 23 | amount: computed, 24 | bounds: computed, 25 | }) 26 | 27 | autorun(this.onUpdate) 28 | } 29 | 30 | get amount() { 31 | return Viewport.isPhone ? 10 : 100 32 | } 33 | 34 | get bounds() { 35 | return DOMUtils.getBounds(this.element) 36 | } 37 | 38 | onUpdate() { 39 | const { scroll } = this.application! 40 | const { top, height } = this.bounds 41 | 42 | const scale = MathUtils.map(top - scroll, -height, Viewport.height, 1, 1.2, true) 43 | const translateY = MathUtils.map(top - scroll, -height, Viewport.height, this.amount, -this.amount, true) 44 | 45 | this.elements.media.style.transform = `translate3d(0, ${translateY}px, 0) scale(${scale})` 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/Reveal.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@lisergia/core' 2 | 3 | export default class Reveal extends Component { 4 | declare classes: { 5 | active: string 6 | } 7 | 8 | declare element: HTMLElement 9 | 10 | constructor({ element }: { element: HTMLElement }) { 11 | super({ 12 | classes: { 13 | active: element.dataset.reveal!, 14 | }, 15 | element, 16 | }) 17 | } 18 | 19 | declare observer: IntersectionObserver 20 | 21 | createObserver() { 22 | this.observer = new IntersectionObserver((entries) => { 23 | entries.forEach((entry) => { 24 | if (entry.isIntersecting) { 25 | this.animateIn() 26 | } else { 27 | this.animateOut() 28 | } 29 | }) 30 | }) 31 | 32 | this.observer.observe(this.element) 33 | } 34 | 35 | destroyObserver() { 36 | this.observer.disconnect() 37 | } 38 | 39 | animateIn() { 40 | this.element.classList.add(this.classes.active) 41 | } 42 | 43 | animateOut() { 44 | this.element.classList.remove(this.classes.active) 45 | } 46 | 47 | addEventListeners() { 48 | this.createObserver() 49 | } 50 | 51 | removeEventListeners() { 52 | this.destroyObserver() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/Source.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@lisergia/core' 2 | 3 | export default class Source extends Component { 4 | declare element: HTMLElement 5 | 6 | constructor({ element }: { element: HTMLElement }) { 7 | super({ 8 | element, 9 | }) 10 | } 11 | 12 | declare observer: IntersectionObserver 13 | 14 | createObserver() { 15 | this.observer = new IntersectionObserver((entries) => { 16 | entries.forEach((entry) => { 17 | if (entry.isIntersecting) { 18 | this.animateIn() 19 | } 20 | }) 21 | }) 22 | 23 | this.observer.observe(this.element) 24 | } 25 | 26 | destroyObserver() { 27 | this.observer.unobserve(this.element) 28 | this.observer.disconnect() 29 | } 30 | 31 | animateIn() { 32 | this.element.onload = () => { 33 | this.element.classList.add('loaded') 34 | } 35 | 36 | this.element.setAttribute('src', this.element.dataset.src!) 37 | 38 | this.removeEventListeners() 39 | } 40 | 41 | addEventListeners() { 42 | this.createObserver() 43 | } 44 | 45 | removeEventListeners() { 46 | this.destroyObserver() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/Title.ts: -------------------------------------------------------------------------------- 1 | import { SplitText } from '@lisergia/utilities' 2 | 3 | import Animation from '../classes/Animation' 4 | 5 | export default class extends Animation { 6 | declare element: HTMLElement 7 | declare elements: { 8 | words: NodeListOf 9 | target: HTMLElement 10 | } 11 | 12 | constructor({ element }: { element: HTMLElement }) { 13 | SplitText.create(element, { 14 | type: 'words', 15 | }) 16 | 17 | super({ 18 | element, 19 | elements: { 20 | words: 'div div', 21 | }, 22 | }) 23 | 24 | const directions = element.dataset.title?.split(',') ?? [] 25 | 26 | this.elements.words.forEach((word, index) => { 27 | word.dataset.direction = directions[index] 28 | }) 29 | 30 | this.animateOut() 31 | } 32 | 33 | animateIn() { 34 | super.animateIn() 35 | 36 | this.elements.words.forEach((word, wordIndex) => { 37 | word.style.transform = 'translate(0, 0)' 38 | word.style.transition = `transform 1.5s ${wordIndex * 0.1}s var(--ease-out-expo)` 39 | }) 40 | } 41 | 42 | animateOut() { 43 | super.animateOut() 44 | 45 | this.elements.words.forEach((word, wordIndex) => { 46 | const direction = word.dataset.direction 47 | 48 | if (direction === 'top') { 49 | word.style.transform = 'translateY(-120%)' 50 | } else if (direction === 'bottom') { 51 | word.style.transform = 'translateY(120%)' 52 | } else if (direction === 'left') { 53 | word.style.transform = 'translateX(-120%)' 54 | } else if (direction === 'right') { 55 | word.style.transform = 'translateX(120%)' 56 | } 57 | 58 | word.style.transition = '' 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/Translate.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class Translate extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | media: HTMLElement 11 | } 12 | 13 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 14 | super({ 15 | application, 16 | element, 17 | elements: { 18 | media: element.firstElementChild as HTMLElement, 19 | }, 20 | }) 21 | 22 | makeObservable(this, { 23 | amount: computed, 24 | bounds: computed, 25 | }) 26 | 27 | autorun(this.onUpdate) 28 | } 29 | 30 | get amount() { 31 | return (Viewport.isPhone ? 10 : 100) * parseFloat(this.element.dataset.translate!) 32 | } 33 | 34 | get bounds() { 35 | return DOMUtils.getBounds(this.element) 36 | } 37 | 38 | onUpdate() { 39 | const { scroll } = this.application! 40 | const { top, height } = this.bounds 41 | 42 | const parallax = MathUtils.map(top - scroll, -height, Viewport.height, this.amount, -this.amount) 43 | 44 | this.elements.media.style.transform = `translate3d(0, ${parallax}px, 0)` 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Categories.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class Categories extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | gallery: HTMLElement 11 | } 12 | 13 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 14 | super({ 15 | application, 16 | element, 17 | elements: { 18 | gallery: '.categories__gallery', 19 | }, 20 | }) 21 | 22 | makeObservable(this, { 23 | bounds: computed, 24 | }) 25 | 26 | autorun(this.onUpdate) 27 | } 28 | 29 | get bounds() { 30 | return DOMUtils.getBounds(this.element) 31 | } 32 | 33 | onUpdate() { 34 | const { scroll } = this.application! 35 | const { height, top } = this.bounds 36 | 37 | const x = MathUtils.map(scroll, top, top + height - Viewport.height, 0, -49, true) 38 | 39 | this.elements.gallery.style.setProperty('--x', `${x}%`) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Details.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | 3 | export default class extends Component { 4 | declare classes: { 5 | buttonActive: string 6 | mediaActive: string 7 | } 8 | 9 | declare element: HTMLElement 10 | declare elements: { 11 | medias: NodeListOf 12 | mediasButtons: NodeListOf 13 | } 14 | 15 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 16 | super({ 17 | classes: { 18 | buttonActive: 'details__gallery__navigation__button--active', 19 | mediaActive: 'details__header__media--active', 20 | }, 21 | element, 22 | elements: { 23 | medias: '.details__header__media', 24 | mediasButtons: '.details__gallery__navigation__button', 25 | }, 26 | }) 27 | } 28 | 29 | onToggle({ target }: MouseEvent) { 30 | const element = target as HTMLElement 31 | const index = parseInt(element.dataset.index!) 32 | 33 | this.elements.medias.forEach((media, mediaIndex) => { 34 | if (mediaIndex === index) { 35 | media.classList.add(this.classes.mediaActive) 36 | } else { 37 | media.classList.remove(this.classes.mediaActive) 38 | } 39 | }) 40 | 41 | this.elements.mediasButtons.forEach((button, buttonIndex) => { 42 | if (buttonIndex === index) { 43 | button.classList.add(this.classes.buttonActive) 44 | } else { 45 | button.classList.remove(this.classes.buttonActive) 46 | } 47 | }) 48 | } 49 | 50 | addEventListeners() { 51 | this.elements.mediasButtons.forEach((button) => { 52 | button.addEventListener('click', this.onToggle, { passive: true }) 53 | }) 54 | } 55 | 56 | removeEventListeners() { 57 | this.elements.mediasButtons.forEach((button) => { 58 | button.removeEventListener('click', this.onToggle) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Footer.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class Footer extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | content: HTMLElement 11 | box: HTMLElement 12 | footer: HTMLElement 13 | } 14 | 15 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 16 | super({ 17 | application, 18 | element, 19 | elements: { 20 | content: '.page__content', 21 | footer: '.page__footer', 22 | }, 23 | }) 24 | 25 | makeObservable(this, { 26 | bounds: computed, 27 | boundsFooter: computed, 28 | }) 29 | 30 | autorun(this.onUpdate) 31 | } 32 | 33 | get bounds() { 34 | return DOMUtils.getBounds(this.element) 35 | } 36 | 37 | get boundsFooter() { 38 | return DOMUtils.getBounds(this.elements.footer) 39 | } 40 | 41 | onUpdate() { 42 | const { scroll } = this.application! 43 | const { top } = this.boundsFooter 44 | 45 | this.elements.footer.style.setProperty('--height', `${this.bounds.height}px`) 46 | 47 | const scale = MathUtils.map(scroll + Viewport.height, top, top + this.bounds.height, 1, 0.95, true) 48 | 49 | this.elements.content.style.transform = `scale(${scale})` 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Hero.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { DOMUtils, MathUtils, SplitText } from '@lisergia/utilities' 3 | 4 | import { createTimeline } from 'animejs' 5 | 6 | import { autorun, computed, makeObservable } from 'mobx' 7 | 8 | export default class Hero extends Component { 9 | declare classes: { 10 | active: string 11 | } 12 | 13 | declare element: HTMLElement 14 | declare elements: { 15 | heroBox: HTMLElement 16 | heroMedia: HTMLElement 17 | heroTitle: HTMLElement 18 | } 19 | 20 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 21 | super({ 22 | application, 23 | classes: { 24 | active: 'hero--active', 25 | }, 26 | element, 27 | elements: { 28 | heroBox: '.hero__box', 29 | heroMedia: '.hero__media', 30 | heroTitle: '.hero__title', 31 | }, 32 | }) 33 | 34 | SplitText.create(this.elements.heroTitle, { 35 | type: 'words', 36 | }) 37 | 38 | SplitText.create(this.elements.heroTitle, { 39 | type: 'words', 40 | }) 41 | 42 | const timeline = createTimeline({ 43 | defaults: { 44 | duration: 2000, 45 | ease: 'inOutCubic', 46 | }, 47 | }) 48 | 49 | timeline.set(this.elements.heroBox, { 50 | '--border': 0, 51 | '--inset': 0, 52 | }) 53 | 54 | timeline.set(this.elements.heroMedia, { 55 | scale: 1.2, 56 | }) 57 | 58 | timeline.label('start', 500) 59 | 60 | timeline.call(() => { 61 | this.element.classList.add(this.classes.active) 62 | }, 1000) 63 | 64 | timeline.add( 65 | this.elements.heroBox, 66 | { 67 | '--border': { to: 1 }, 68 | '--inset': { to: 1 }, 69 | }, 70 | 'start', 71 | ) 72 | 73 | timeline.add( 74 | this.elements.heroMedia, 75 | { 76 | scale: { to: 1 }, 77 | }, 78 | 'start', 79 | ) 80 | 81 | timeline.play() 82 | 83 | makeObservable(this, { 84 | bounds: computed, 85 | }) 86 | 87 | autorun(this.onUpdate) 88 | } 89 | 90 | get bounds() { 91 | return DOMUtils.getBounds(this.element) 92 | } 93 | 94 | onUpdate() { 95 | const { scroll } = this.application! 96 | const { height, top } = this.bounds 97 | 98 | const scale = MathUtils.map(scroll, top, top + height, 1, 1.5) 99 | const translateY = MathUtils.map(scroll, top, top + height, 0, 100) 100 | 101 | this.elements.heroMedia.style.transform = `translate3d(0, ${translateY}px, 0) scale(${scale})` 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/List.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class List extends Component { 8 | declare classes: { 9 | active: string 10 | } 11 | 12 | declare element: HTMLElement 13 | declare elements: { 14 | categories: HTMLElement 15 | } 16 | 17 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 18 | super({ 19 | application, 20 | classes: { 21 | active: 'list--active', 22 | }, 23 | element, 24 | elements: { 25 | categories: '.categories', 26 | }, 27 | }) 28 | 29 | makeObservable(this, { 30 | bounds: computed, 31 | }) 32 | 33 | autorun(this.onUpdate) 34 | } 35 | 36 | get bounds() { 37 | return DOMUtils.getBounds(this.element) 38 | } 39 | 40 | onUpdate() { 41 | const { scroll } = this.application! 42 | const { top } = this.bounds 43 | const { width } = Viewport 44 | 45 | if (scroll >= top + width * 0.6) { 46 | this.element.classList.add(this.classes.active) 47 | } else { 48 | this.element.classList.remove(this.classes.active) 49 | } 50 | 51 | const x = MathUtils.map(scroll, top, top + width, 0, -100, true) 52 | const y = MathUtils.map(scroll, top, top + width, 0, 100, true) 53 | 54 | this.elements.categories.style.setProperty('--x', `${x}%`) 55 | this.elements.categories.style.setProperty('--y', `${y}vw`) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Marquee.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import Tempus from 'tempus' 6 | 7 | export default class extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | list: HTMLElement 11 | items: NodeListOf< 12 | HTMLElement & { 13 | extra: number 14 | width: number 15 | offset: number 16 | position: number 17 | isBefore: boolean 18 | isAfter: boolean 19 | clamp: number 20 | } 21 | > 22 | } 23 | 24 | declare length: number 25 | declare widthTotal: number 26 | 27 | multiplier = 1 28 | scroll = { 29 | clamp: 0, 30 | current: 0, 31 | ease: 0.1, 32 | last: 0, 33 | position: 0, 34 | target: 0, 35 | } 36 | 37 | declare unsubscribeRaf: (() => void) | undefined 38 | 39 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 40 | super({ 41 | application, 42 | element, 43 | elements: { 44 | list: '.marquee__wrapper', 45 | items: 'span', 46 | }, 47 | }) 48 | 49 | this.widthTotal = this.elements.list.getBoundingClientRect().width 50 | 51 | this.reset() 52 | 53 | Viewport.on('resize', this.onResize) 54 | 55 | this.unsubscribeRaf = Tempus.add(this.onUpdate) 56 | } 57 | 58 | transform(element: HTMLElement, x: number) { 59 | element.style.transform = `translate3d(${Math.floor(x)}px, 0, 0)` 60 | } 61 | 62 | declare direction: 'up' | 'down' 63 | 64 | onUpdate() { 65 | this.scroll.target += this.multiplier 66 | this.scroll.current = MathUtils.lerp(this.scroll.current, this.scroll.target, this.scroll.ease) 67 | 68 | const scrollClamp = Math.round(this.scroll.current % this.widthTotal) 69 | 70 | if (this.scroll.current < this.scroll.last) { 71 | this.direction = 'down' 72 | } else { 73 | this.direction = 'up' 74 | } 75 | 76 | this.elements.items.forEach((element, index) => { 77 | element.position = -this.scroll.current - element.extra 78 | 79 | const bounds = element.position + element.offset + element.width 80 | 81 | element.isBefore = bounds < 0 82 | element.isAfter = bounds > this.widthTotal 83 | 84 | if (this.direction === 'up' && element.isBefore) { 85 | element.extra = element.extra - this.widthTotal 86 | 87 | element.isBefore = false 88 | element.isAfter = false 89 | } 90 | 91 | if (this.direction === 'down' && element.isAfter) { 92 | element.extra = element.extra + this.widthTotal 93 | 94 | element.isBefore = false 95 | element.isAfter = false 96 | } 97 | 98 | element.clamp = element.extra % scrollClamp 99 | 100 | this.transform(element, element.position) 101 | }) 102 | 103 | this.scroll.last = this.scroll.current 104 | this.scroll.clamp = scrollClamp 105 | } 106 | 107 | onResize() { 108 | this.widthTotal = this.elements.list.getBoundingClientRect().width 109 | 110 | this.reset() 111 | 112 | this.scroll = { 113 | clamp: 0, 114 | current: 0, 115 | ease: 0.1, 116 | last: 0, 117 | position: 0, 118 | target: 0, 119 | } 120 | } 121 | 122 | reset() { 123 | this.elements.items.forEach((element) => { 124 | this.transform(element, 0) 125 | 126 | const bounds = DOMUtils.getBounds(element) 127 | 128 | element.extra = 0 129 | element.offset = bounds.left 130 | element.position = 0 131 | element.width = bounds.width 132 | }) 133 | } 134 | 135 | destroy() { 136 | super.destroy() 137 | 138 | Viewport.off('resize', this.onResize) 139 | 140 | this.unsubscribeRaf?.() 141 | this.unsubscribeRaf = undefined 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Media.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class Hero extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | mediaVideo: HTMLElement 11 | } 12 | 13 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 14 | super({ 15 | application, 16 | element, 17 | elements: { 18 | mediaVideo: '.media__video', 19 | }, 20 | }) 21 | 22 | makeObservable(this, { 23 | bounds: computed, 24 | }) 25 | 26 | autorun(this.onUpdate) 27 | } 28 | 29 | get bounds() { 30 | return DOMUtils.getBounds(this.element) 31 | } 32 | 33 | onUpdate() { 34 | const { scroll } = this.application! 35 | const { height, top } = this.bounds 36 | 37 | const headerScale = MathUtils.map(scroll, top - Viewport.height, top + height, 1, 1.5, true) 38 | const headerY = scroll - top 39 | 40 | this.elements.mediaVideo.style.transform = `translate3d(0, ${headerY}px, 0) scale(${headerScale})` 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Seasons.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class Seasons extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | highlight: HTMLElement 11 | media1: HTMLElement 12 | media2: HTMLElement 13 | media3: HTMLElement 14 | media4: HTMLElement 15 | } 16 | 17 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 18 | super({ 19 | application, 20 | element, 21 | elements: { 22 | highlight: '.seasons__highlight', 23 | media1: '.seasons__gallery__media--1', 24 | media2: '.seasons__gallery__media--2', 25 | media3: '.seasons__gallery__media--3', 26 | media4: '.seasons__gallery__media--4', 27 | }, 28 | }) 29 | 30 | makeObservable(this, { 31 | bounds: computed, 32 | }) 33 | 34 | autorun(this.onUpdate) 35 | } 36 | 37 | get bounds() { 38 | return DOMUtils.getBounds(this.element) 39 | } 40 | 41 | scrollSpeed = 0 42 | scrollLast = 0 43 | 44 | onUpdate() { 45 | const { scroll } = this.application! 46 | const { height } = Viewport 47 | 48 | const scrollOffset = scroll - this.bounds.top 49 | 50 | const skewX = MathUtils.clamp(-10, 10, this.scrollSpeed) 51 | const translateX = MathUtils.map(scrollOffset, 0, height * 3, 11, 88) 52 | 53 | this.elements.highlight.style.transform = `translateX(-${translateX}%) skewX(${skewX}deg)` 54 | 55 | this.elements.media1.style.opacity = `${MathUtils.map(scrollOffset, 0, height, 1, 0, true)}` 56 | this.elements.media1.style.transform = `scale(${MathUtils.map(scrollOffset, 0, height, 1, 1.5, true)})` 57 | 58 | this.elements.media2.style.opacity = `${MathUtils.map(scrollOffset, height, height * 2, 1, 0)}` 59 | this.elements.media2.style.transform = `scale(${MathUtils.map(scrollOffset, height, height * 2, 1, 1.5, true)})` 60 | 61 | this.elements.media3.style.opacity = `${MathUtils.map(scrollOffset, height * 2, height * 3, 1, 0)}` 62 | this.elements.media3.style.transform = `scale(${MathUtils.map(scrollOffset, height * 2, height * 3, 1, 1.5, true)})` 63 | 64 | this.scrollSpeed = scroll - this.scrollLast 65 | this.scrollLast = scroll 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /apps/web/src/app/datasets/sections/Shop.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager, Component } from '@lisergia/core' 2 | import { Viewport } from '@lisergia/managers' 3 | import { DOMRectBounds, DOMUtils, MathUtils } from '@lisergia/utilities' 4 | 5 | import { autorun, computed, makeObservable } from 'mobx' 6 | 7 | export default class Shop extends Component { 8 | declare element: HTMLElement 9 | declare elements: { 10 | header: HTMLElement 11 | categories: NodeListOf 12 | } 13 | 14 | constructor({ application, element }: { application: ApplicationManager; element: HTMLElement }) { 15 | super({ 16 | application, 17 | element, 18 | elements: { 19 | header: '.shop__header__titles__wrapper', 20 | categories: '.shop__category', 21 | }, 22 | }) 23 | 24 | Viewport.on('resize', this.onResize) 25 | 26 | autorun(this.onUpdate) 27 | } 28 | 29 | onResize() { 30 | this.elements.categories.forEach((category) => { 31 | category.bounds = DOMUtils.getBounds(category) 32 | }) 33 | } 34 | 35 | onUpdate() { 36 | const { scroll } = this.application! 37 | 38 | let index = 0 39 | 40 | this.elements.categories.forEach((category, categoryIndex) => { 41 | if (scroll + Viewport.height > category.bounds?.top) { 42 | index = categoryIndex 43 | } 44 | }) 45 | 46 | this.elements.header.style.transform = `translateY(-${100 * index}%)` 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/src/app/templates/Standard.ts: -------------------------------------------------------------------------------- 1 | import { Page, PageParameters } from '@lisergia/core' 2 | 3 | export default class extends Page { 4 | declare element: HTMLElement 5 | 6 | constructor(args: PageParameters) { 7 | super({ 8 | ...args, 9 | element: '.page:last-child', 10 | elements: { 11 | wrapper: '.page__wrapper', 12 | }, 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/shared/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web/src/shared/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /apps/web/src/shared/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/favicon/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/shared/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Garoa Skincare", 3 | "short_name": "Garoa", 4 | "icons": [ 5 | { 6 | "src": "/favicon/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "/favicon/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "any maskable" 16 | } 17 | ], 18 | "theme_color": "#141414", 19 | "background_color": "#141414", 20 | "display": "standalone", 21 | "start_url": "/" 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/shared/favicon/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/favicon/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /apps/web/src/shared/favicon/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/favicon/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /apps/web/src/shared/fonts/editorial-new.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/fonts/editorial-new.woff -------------------------------------------------------------------------------- /apps/web/src/shared/fonts/editorial-new.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/fonts/editorial-new.woff2 -------------------------------------------------------------------------------- /apps/web/src/shared/fonts/neue-montreal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/fonts/neue-montreal.woff -------------------------------------------------------------------------------- /apps/web/src/shared/fonts/neue-montreal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/fonts/neue-montreal.woff2 -------------------------------------------------------------------------------- /apps/web/src/shared/open-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/lisergia/7b4024f401856c924702403447846fd6b6b5376e/apps/web/src/shared/open-graph.png -------------------------------------------------------------------------------- /apps/web/src/shared/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /apps/web/src/sprites/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/web/src/sprites/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/web/src/styles/base/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-display: swap; 3 | font-family: $font-neue-montreal; 4 | font-style: normal; 5 | font-weight: normal; 6 | src: 7 | url('/fonts/neue-montreal.woff2') format('woff2'), 8 | url('/fonts/neue-montreal.woff') format('woff'); 9 | } 10 | 11 | @font-face { 12 | font-display: swap; 13 | font-family: $font-editorial-new; 14 | font-style: normal; 15 | font-weight: normal; 16 | src: 17 | url('/fonts/editorial-new.woff2') format('woff2'), 18 | url('/fonts/editorial-new.woff') format('woff'); 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/src/styles/base/reset.scss: -------------------------------------------------------------------------------- 1 | html { 2 | background: var(--color-black); 3 | font-size: calc(100vw / 1440 * 10); 4 | 5 | @include media('=desktop') { 19 | font-size: 16px; 20 | } 21 | } 22 | 23 | ::-webkit-scrollbar { 24 | display: none; 25 | opacity: 0; 26 | visibility: hidden; 27 | } 28 | 29 | .app { 30 | border-radius: 0; 31 | height: 100%; 32 | top: 0; 33 | transition: all 1s ease; 34 | z-index: z('box'); 35 | 36 | .navigation--open & { 37 | background: var(--color-cod-gray); 38 | border-radius: 1rem; 39 | height: calc(100% - 7.5rem - 2.5rem); 40 | pointer-events: none; 41 | top: 7.5rem; 42 | transform: translateX(62.5rem); 43 | } 44 | 45 | @include media('>=desktop') { 46 | .navigation--open & { 47 | transform: translateX(625px); 48 | top: 75px; 49 | height: calc(100% - 75px - 25px); 50 | } 51 | } 52 | } 53 | 54 | [data-paragraph] { 55 | div { 56 | display: inline-block; 57 | overflow: hidden; 58 | vertical-align: bottom; 59 | } 60 | } 61 | 62 | img { 63 | opacity: 0; 64 | transition: opacity 1s ease; 65 | 66 | &.loaded { 67 | opacity: 1; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/web/src/styles/components/bag.scss: -------------------------------------------------------------------------------- 1 | .bag { 2 | background: var(--color-cod-gray); 3 | border-radius: 1rem; 4 | color: var(--color-white); 5 | max-width: calc(100vw - 3rem); 6 | opacity: 0; 7 | position: absolute; 8 | right: 1.6rem; 9 | top: 1.6rem; 10 | transform: translateY(50px); 11 | transition: 12 | opacity 1s var(--ease-out-expo), 13 | transform 1s var(--ease-out-expo), 14 | visibility 1s var(--ease-out-expo); 15 | visibility: hidden; 16 | width: 40rem; 17 | z-index: z('bag'); 18 | 19 | .bag--active & { 20 | opacity: 1; 21 | transform: translateY(0); 22 | visibility: visible; 23 | } 24 | 25 | @include media(' * { 22 | pointer-events: auto; 23 | } 24 | } 25 | 26 | .page__footer { 27 | height: var(--height, 0); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/src/styles/sections/categories.scss: -------------------------------------------------------------------------------- 1 | .categories { 2 | color: var(--color-white); 3 | height: calc(3 * var(--100vh, 100vh)); 4 | position: relative; 5 | text-align: center; 6 | transform: translate(var(--x, 0), var(--y, 0)); 7 | } 8 | 9 | .categories__wrapper { 10 | height: var(--100vh, 100vh); 11 | position: sticky; 12 | top: 0; 13 | } 14 | 15 | .categories__content { 16 | left: 50%; 17 | position: absolute; 18 | top: 50%; 19 | transform: translate(-50%, -50%); 20 | z-index: 1; 21 | } 22 | 23 | .categories__label { 24 | @extend %label-14; 25 | 26 | @extend %animation-letter-spacing; 27 | 28 | &--active { 29 | @extend %animation-letter-spacing--active; 30 | } 31 | } 32 | 33 | .categories__title { 34 | font: 10.8rem/0.9 $font-editorial-new; 35 | margin-top: 1.5rem; 36 | overflow: hidden; 37 | width: 60rem; 38 | 39 | @include media('>=desktop') { 40 | font-size: 108px; 41 | margin-top: 15px; 42 | width: 600px; 43 | } 44 | 45 | @include media('=desktop') { 12 | width: calc(100% - 80px); 13 | } 14 | 15 | @include media('=desktop') { 58 | margin-top: 15px; 59 | padding-bottom: 13px; 60 | width: 300px; 61 | 62 | .columns--left & { 63 | width: 460px; 64 | } 65 | } 66 | 67 | @include media('=desktop') { 83 | margin-top: 50px; 84 | max-width: 410px; 85 | } 86 | 87 | @include media('=desktop') { 36 | font-size: 14px; 37 | width: 200px; 38 | } 39 | } 40 | 41 | .disclaimer__poem__author { 42 | font: 1.8rem $font-editorial-new; 43 | margin-top: 3.2rem; 44 | 45 | @include media('>=desktop') { 46 | font-size: 18px; 47 | margin-top: 32px; 48 | } 49 | 50 | @include media('=desktop') { 67 | width: 460px; 68 | 69 | p:not(:first-child) { 70 | margin-top: 25px; 71 | } 72 | } 73 | 74 | @include media('=desktop') { 67 | font-size: 90px; 68 | width: 720px; 69 | 70 | span:nth-child(1), 71 | span:nth-child(5) { 72 | padding-right: 10px; 73 | } 74 | } 75 | 76 | @include media('=desktop') { 39 | margin-top: 15px; 40 | width: 400px; 41 | } 42 | 43 | @include media('=desktop') { 55 | margin-top: 50px; 56 | width: 320px; 57 | } 58 | 59 | @include media('=desktop') { 6 | padding: 40px; 7 | } 8 | 9 | @include media('=desktop') { 56 | font-size: 14px; 57 | margin-top: 35px; 58 | max-width: 590px; 59 | } 60 | 61 | @include media('=desktop') { 75 | font-size: 14px; 76 | margin-top: 50px; 77 | width: 665px; 78 | } 79 | 80 | @include media('=desktop') { 98 | padding: 9px 30px; 99 | } 100 | } 101 | 102 | .ingredients__item__dash { 103 | @extend %dash; 104 | 105 | &--active { 106 | @extend %dash--active; 107 | } 108 | 109 | @include media('=desktop') { 119 | width: 210px; 120 | } 121 | 122 | @include media('=desktop') { 134 | width: 210px; 135 | } 136 | 137 | @include media('=desktop') { 10 | padding-top: 80px; 11 | } 12 | 13 | @include media('=desktop') { 101 | font-size: 78px; 102 | } 103 | 104 | @include media('=desktop') { 17 | max-width: 1040px; 18 | } 19 | 20 | @include media('=desktop') { 53 | font-size: 46px; 54 | 55 | .quote--active & { 56 | span:nth-of-type(1) { 57 | transform: translateX(-53px); 58 | } 59 | 60 | span:nth-of-type(2) { 61 | transform: translateX(52px); 62 | } 63 | 64 | span:nth-of-type(5) { 65 | transform: translateX(-52px); 66 | } 67 | 68 | span:nth-of-type(6) { 69 | transform: translateX(52px); 70 | } 71 | } 72 | } 73 | 74 | @include media('=desktop') { 133 | height: 80px; 134 | width: 80px; 135 | 136 | &:nth-of-type(1) { 137 | left: 165px; 138 | top: -8px; 139 | } 140 | 141 | &:nth-of-type(2) { 142 | left: 1018px; 143 | top: 52px; 144 | } 145 | 146 | &:nth-of-type(3) { 147 | left: 590px; 148 | top: 175px; 149 | } 150 | } 151 | 152 | @include media('=desktop') { 27 | bottom: 40px; 28 | padding-bottom: 195px; 29 | left: 40px; 30 | right: 40px; 31 | top: 75px; 32 | } 33 | 34 | @include media('=desktop') { 56 | margin-top: 10px; 57 | width: 337.5px; 58 | } 59 | 60 | @include media('=desktop') { 80 | bottom: 90px; 81 | font-size: 120px; 82 | } 83 | } 84 | 85 | .seasons__gallery { 86 | @extend %cover; 87 | 88 | z-index: -1; 89 | } 90 | 91 | .seasons__gallery__media { 92 | @extend %cover; 93 | 94 | &--1 { 95 | z-index: 4; 96 | } 97 | 98 | &--2 { 99 | z-index: 3; 100 | } 101 | 102 | &--3 { 103 | z-index: 2; 104 | } 105 | 106 | &--4 { 107 | z-index: 1; 108 | } 109 | } 110 | 111 | .seasons__gallery__media__image { 112 | @extend %cover; 113 | 114 | object-fit: cover; 115 | } 116 | -------------------------------------------------------------------------------- /apps/web/src/styles/shared/animations.scss: -------------------------------------------------------------------------------- 1 | %animation-letter-spacing { 2 | letter-spacing: 0.5rem; 3 | opacity: 0; 4 | transition: 5 | opacity 1s var(--ease-out-expo), 6 | letter-spacing 1s var(--ease-out-expo); 7 | 8 | &--active { 9 | letter-spacing: 0.05rem; 10 | opacity: 1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/styles/shared/button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | align-items: center; 3 | border-radius: 2.4rem; 4 | color: var(--color, var(--color-white)); 5 | display: flex; 6 | height: 4rem; 7 | justify-content: center; 8 | overflow: hidden; 9 | position: relative; 10 | transform: translateY(100%); 11 | transition: 12 | color 1s var(--ease-out-expo), 13 | transform 0.6s ease; 14 | width: 14rem; 15 | 16 | &--active { 17 | transform: translateY(0); 18 | } 19 | 20 | &::after { 21 | background: var(--color, var(--color-white)); 22 | border-radius: 50%; 23 | bottom: 0; 24 | content: ''; 25 | display: block; 26 | height: 14rem; 27 | left: 50%; 28 | opacity: 0; 29 | position: absolute; 30 | top: 50%; 31 | transform: translate(-50%, -50%) scale(0.75); 32 | transition: 33 | opacity 0.5s cubic-bezier(0.26, 1, 0.48, 1), 34 | transform 0.5s cubic-bezier(0.26, 1, 0.48, 1); 35 | width: 14rem; 36 | z-index: 1; 37 | } 38 | 39 | &:hover { 40 | transform: scale(1.05); 41 | 42 | &::after { 43 | opacity: 1; 44 | transform: translate(-50%, -50%) scale(1); 45 | } 46 | } 47 | 48 | @include media('>=desktop') { 49 | height: 40px; 50 | border-radius: 24px; 51 | width: 140px; 52 | 53 | &:after { 54 | border-radius: 50%; 55 | } 56 | } 57 | } 58 | 59 | .button__background { 60 | background: var(--background, var(--color-black)); 61 | border-radius: 2.4rem; 62 | height: 100%; 63 | left: 50%; 64 | position: absolute; 65 | transform: translateX(-50%); 66 | transition: width 1s ease; 67 | width: 4rem; 68 | 69 | .button--active & { 70 | transition: width 1s 0.6s ease; 71 | width: 100%; 72 | } 73 | 74 | @include media('>=desktop') { 75 | border-radius: 24px; 76 | width: 40px; 77 | 78 | .button--active & { 79 | width: 100%; 80 | } 81 | } 82 | } 83 | 84 | .button__text { 85 | display: block; 86 | overflow: hidden; 87 | position: relative; 88 | z-index: 2; 89 | } 90 | 91 | .button__text__line { 92 | display: block; 93 | font: 1.2rem $font-anonymous; 94 | letter-spacing: 0.05rem; 95 | opacity: 0; 96 | position: relative; 97 | text-transform: uppercase; 98 | transform: translateY(100%); 99 | transition: transform 1s ease; 100 | z-index: 1; 101 | 102 | .button--active & { 103 | opacity: 1; 104 | transform: translateY(0); 105 | transition: 106 | opacity 1s 0.7s ease, 107 | transform 1s var(--ease-out-expo); 108 | } 109 | 110 | &::after { 111 | color: var(--background, var(--color-black)); 112 | content: attr(data-text); 113 | left: 0; 114 | position: absolute; 115 | top: 100%; 116 | } 117 | 118 | .button:hover & { 119 | transform: translateY(-100%) !important; 120 | } 121 | 122 | @include media('>=desktop') { 123 | font-size: 12px; 124 | letter-spacing: 0.5px; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /apps/web/src/styles/shared/links.scss: -------------------------------------------------------------------------------- 1 | %link__wrapper { 2 | display: inline-block; 3 | overflow: hidden; 4 | position: relative; 5 | vertical-align: top; 6 | } 7 | 8 | %link__line { 9 | background: currentcolor; 10 | bottom: 0; 11 | content: ''; 12 | height: 1px; 13 | left: 0; 14 | position: absolute; 15 | transition: transform 0.7s var(--ease-out-expo); 16 | width: 100%; 17 | } 18 | 19 | %link__line--hidden { 20 | transform: scaleX(0); 21 | transform-origin: right center; 22 | } 23 | 24 | %link__line--visible { 25 | transform: scaleX(1); 26 | transform-origin: left center; 27 | } 28 | 29 | %link { 30 | @extend %link__wrapper; 31 | 32 | display: inline-block; 33 | line-height: 1.3; 34 | 35 | &::after { 36 | @extend %link__line; 37 | @extend %link__line--visible; 38 | } 39 | 40 | &:hover, 41 | &:focus { 42 | &::after { 43 | @extend %link__line--hidden; 44 | } 45 | } 46 | } 47 | 48 | %link--hidden { 49 | @extend %link__wrapper; 50 | 51 | display: inline-block; 52 | 53 | &::after { 54 | @extend %link__line; 55 | @extend %link__line--hidden; 56 | } 57 | 58 | &:hover, 59 | &:focus { 60 | &::after { 61 | @extend %link__line--visible; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/web/src/styles/shared/sections.scss: -------------------------------------------------------------------------------- 1 | %label-12 { 2 | font: 1.2rem $font-anonymous; 3 | text-transform: uppercase; 4 | 5 | @include media('>=desktop') { 6 | font-size: 12px; 7 | } 8 | } 9 | 10 | %label-14 { 11 | font: 1.4rem $font-anonymous; 12 | text-transform: uppercase; 13 | 14 | @include media('>=desktop') { 15 | font-size: 14px; 16 | } 17 | } 18 | 19 | %title-70 { 20 | font: 7rem/0.9 $font-editorial-new; 21 | 22 | @include media('>=desktop') { 23 | font-size: 70px; 24 | } 25 | 26 | @include media('=desktop') { 35 | font-size: 18px; 36 | } 37 | 38 | @include media(' page.slug.current === slug) 22 | 23 | if (!data) { 24 | data = pages.find((page) => page.slug.current === 'not-found') 25 | } 26 | 27 | return { 28 | analytics, 29 | typekit, 30 | 31 | categories, 32 | footer, 33 | menu, 34 | settings, 35 | 36 | ...data, 37 | 38 | isDesktop, 39 | isPhone, 40 | isTablet, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "index.js", 6 | "use": "@vercel/node" 7 | }, 8 | { 9 | "src": "**/*", 10 | "use": "@vercel/static" 11 | } 12 | ], 13 | "rewrites": [ 14 | { 15 | "source": "/(.*)", 16 | "destination": "index.js" 17 | } 18 | ], 19 | "outputDirectory": "." 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/views/base.twig: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | {% include 'shared/header.twig' %} 10 | 11 | 12 | {% include 'layout/menu.twig' %} 13 | {% include 'layout/navigation.twig' %} 14 | 15 |
16 | {% block content %} 17 | 18 | {% endblock %} 19 |
20 | 21 | {% include 'shared/scripts.twig' %} 22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/web/views/components/button.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ text }} 5 | 6 | -------------------------------------------------------------------------------- /apps/web/views/layout/menu.twig: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /apps/web/views/pages/page.twig: -------------------------------------------------------------------------------- 1 | {% extends '../base.twig' %} 2 | 3 | {% set template = 'page' %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | {% include '../sections/index.twig' %} 10 |
11 | 12 | 13 | 14 | {% include '../layout/footer.twig' %} 15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /apps/web/views/sections/_categories.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | {{ section.label }} 6 |

7 | 8 |

{{ section.title }}

9 |
10 | 11 | 48 |
49 |
50 | -------------------------------------------------------------------------------- /apps/web/views/sections/_columns.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% for item in section.list %} 4 | {% set asset = getAsset(item.asset) %} 5 | 6 |
7 | {{ asset.alt }} 14 |
15 | {% endfor %} 16 |
17 | 18 |
19 |
20 | 23 | 24 |

28 | {{ section.title }} 29 |

30 | 31 |

32 | {{ section.description }} 33 |

34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /apps/web/views/sections/_contact.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {% for item in section.list %} 5 |
6 |

{{ item.title }}

7 | 8 |
9 | {{ parseHTML(item.description) }} 10 |
11 |
12 | {% endfor %} 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /apps/web/views/sections/_details.twig: -------------------------------------------------------------------------------- 1 |
2 | 34 | 35 |
36 |

37 | {{ label }} 38 |

39 | 40 |

{{ title }}

41 | 42 |
43 | {{ 44 | parseHTML(section.description)|replace({ 45 | '

': '

', 46 | '

    ': '
      ', 47 | '
    • ': '
    • ', 48 | '
    • ': '
' 49 | }) 50 | }} 51 |
52 | 53 | {% include '../components/button.twig' with { 54 | class: 'details__information__button', 55 | text: 'Add to Your Bag — ' ~ price, 56 | value: details.slug.current 57 | } %} 58 | 59 |
60 | -------------------------------------------------------------------------------- /apps/web/views/sections/_disclaimer.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 11 | 14 | 15 | 16 |

17 | {{ section.title }} 18 |

19 | 20 |

21 | {{ section.author }} 22 |

23 |
24 | 25 |
26 | {{ 27 | parseHTML(section.description)|replace({ 28 | '

': '

' 29 | }) 30 | }} 31 |

32 |
33 |
34 | -------------------------------------------------------------------------------- /apps/web/views/sections/_error.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% set asset = getAsset(section.image) %} 4 | 5 | {{ asset.alt }} 6 |
7 | 8 |
9 |
10 |

{{ section.title }}

11 | 12 |

13 | {{ section.description }} 14 |

15 | 16 |
17 | {% include '../components/button.twig' with { 18 | class: 'error__button__element', 19 | text: section.button.text, 20 | url: section.button.url 21 | } %} 22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /apps/web/views/sections/_gallery.twig: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /apps/web/views/sections/_header.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {% set asset = getAsset(section.image) %} 5 | 6 | {{ asset.alt }} 13 |
14 | 15 |
16 |

17 | {{ section.title }} 18 |

19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /apps/web/views/sections/_hero.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {% set asset = getAsset(section.image) %} 5 | 6 | {{ asset.alt }} 13 |
14 | 15 |
16 |

{{ section.title }}

17 | 18 |
19 | {% include '../components/button.twig' with { 20 | class: 'hero__button__element', 21 | text: section.button.text, 22 | url: section.button.url 23 | } %} 24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /apps/web/views/sections/_highlight.twig: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ section.label }} 4 |

5 | 6 |

{{ section.title }}

7 | 8 |

9 | {{ section.description }} 10 |

11 | 12 | {% for media in section.list %} 13 | {% set speed = 0 %} 14 | 15 | {% if loop.index == 1 %} 16 | {% set speed = 1.5 %} 17 | {% elseif loop.index == 2 %} 18 | {% set speed = -0.5 %} 19 | {% elseif loop.index == 3 %} 20 | {% set speed = 1 %} 21 | {% elseif loop.index == 4 %} 22 | {% set speed = -2 %} 23 | {% endif %} 24 | 25 |
26 |
27 | {% set asset = getAsset(media) %} 28 | 29 | {{ asset.alt }} 36 |
37 |
38 | {% endfor %} 39 |
40 | -------------------------------------------------------------------------------- /apps/web/views/sections/_information.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% set asset = getAsset(section.image.asset) %} 4 | 5 |
6 | {{ asset.alt }} 13 |
14 |
15 | 16 |
17 |
18 | {{ 19 | parseHTML(section.description)|replace({ 20 | '

': '

', 21 | '

': '

', 22 | '

    ': '
      ', 23 | '
    • ': '
    • ', 24 | '
      ': '
      ', 25 | '
    • ': '

' 26 | }) 27 | }} 28 |
29 | 30 |
31 | -------------------------------------------------------------------------------- /apps/web/views/sections/_ingredients.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

{{ section.title }}

5 | 6 |
7 | {{ parseHTML(section.description) }} 8 |
9 | 10 |
11 | {% for ingredient in section.list %} 12 |
13 |

14 | {{ ingredient.title }} 15 |

16 | 17 |

18 | {{ ingredient.region }} 19 |

20 | 21 |

22 | {{ ingredient.ingredient }} 23 |

24 | 25 | 26 |
27 | {% endfor %} 28 |
29 |
30 | 31 |
32 | {% set asset = getAsset(section.image) %} 33 | 34 | {{ asset.alt }} 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /apps/web/views/sections/_intro.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ section.label }} 5 |

6 | 7 |

{{ section.title }}

8 | 9 |

10 | {{ section.description }} 11 |

12 |
13 | 14 |
15 | {% set asset = getAsset(section.image) %} 16 | 17 | {{ asset.alt }} 18 |
19 |
20 | -------------------------------------------------------------------------------- /apps/web/views/sections/_list.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 | {# TODO: Implement medias. #} 4 | {#
5 | {% for item in section.list %} 6 |
7 | {% set asset = getAsset(item.image.asset) %} 8 | 9 | {{ asset.alt }} 16 |
17 | {% endfor %} 18 |
#} 19 | 20 |

21 | {{ section.label }} 22 |

23 | 24 |
    25 | {% for item in section.list %} 26 |
  • 27 | {{ item.text }} 28 |
  • 29 | {% endfor %} 30 |
31 | 32 |
33 | {% include '../components/button.twig' with { 34 | class: 'list__button__element', 35 | text: section.button.text, 36 | url: section.button.url 37 | } %} 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /apps/web/views/sections/_lookbook.twig: -------------------------------------------------------------------------------- 1 | {% for entry in section.list %} 2 | {% set index = loop.index %} 3 | 4 |
5 | {% for item in entry.content %} 6 | {% if item.entry.type == 'image' %} 7 |
8 | {% set asset = getAsset(item.entry.image) %} 9 | 10 | {{ asset.alt }} 17 |
18 | {% endif %} 19 | 20 | {% if item.entry.type == 'text' %} 21 |
22 |
23 | {{ parseHTML(item.entry.title) }} 24 |
25 |
26 | {% endif %} 27 | {% endfor %} 28 |
29 | {% endfor %} 30 | -------------------------------------------------------------------------------- /apps/web/views/sections/_marquee.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% for i in 1..10 %} 4 | {{ section.title }}  5 | {% endfor %} 6 |
7 |
8 | -------------------------------------------------------------------------------- /apps/web/views/sections/_media.twig: -------------------------------------------------------------------------------- 1 |
2 | {% set asset = getFile(section.media) %} 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /apps/web/views/sections/_quote.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 | {{ 6 | section.title|replace({ 7 | '|': '' 8 | }) 9 | }} 10 | 11 |

12 | 13 | {% for item in section.list %} 14 |
15 | {% set asset = getAsset(item.asset) %} 16 | 17 | {{ asset.alt }} 18 |
19 | {% endfor %} 20 |
21 |
22 | -------------------------------------------------------------------------------- /apps/web/views/sections/_seasons.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 6 | {{ 7 | section.title|replace({ 8 | '-': '-' 9 | }) 10 | }} 11 | 12 |

13 | 14 |

15 | {{ section.description }} 16 |

17 | 18 |

19 | {{ section.label }} 20 |

21 | 22 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /apps/web/views/sections/_shop.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | {% for category in categories %} 7 | 8 | {{ category.title }} 9 | 10 | {# TODO: Add filter to list how many products are in a category. #} 11 | ({{ 3 }}) 12 | 13 | {% endfor %} 14 | 15 | 16 |
17 | 18 |
19 | {% for category in section.list %} 20 |
21 | {% for item in category.content %} 22 | {% if item.entry.type == 'product' %} 23 | 54 | {% elseif item.entry.type == 'image' %} 55 |
56 | {% set asset = getAsset(item.entry.image.asset) %} 57 | 58 | {{ asset.alt }} 65 |
66 | {% endif %} 67 | {% endfor %} 68 |
69 | {% endfor %} 70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /apps/web/views/sections/index.twig: -------------------------------------------------------------------------------- 1 | {% for section in content %} 2 | {% set layout = section._type %} 3 | 4 | {% if layout == 'categories' %} 5 | {% include './_categories.twig' with section %} 6 | {% elseif layout == 'columns' %} 7 | {% include './_columns.twig' with section %} 8 | {% elseif layout == 'contact' %} 9 | {% include './_contact.twig' with section %} 10 | {% elseif layout == 'details' %} 11 | {% include './_details.twig' with section %} 12 | {% elseif layout == 'disclaimer' %} 13 | {% include './_disclaimer.twig' with section %} 14 | {% elseif layout == 'error' %} 15 | {% include './_error.twig' with section %} 16 | {% elseif layout == 'gallery' %} 17 | {% include './_gallery.twig' with section %} 18 | {% elseif layout == 'header' %} 19 | {% include './_header.twig' with section %} 20 | {% elseif layout == 'hero' %} 21 | {% include './_hero.twig' with section %} 22 | {% elseif layout == 'highlight' %} 23 | {% include './_highlight.twig' with section %} 24 | {% elseif layout == 'information' %} 25 | {% include './_information.twig' with section %} 26 | {% elseif layout == 'ingredients' %} 27 | {% include './_ingredients.twig' with section %} 28 | {% elseif layout == 'intro' %} 29 | {% include './_intro.twig' with section %} 30 | {% elseif layout == 'list' %} 31 | {% include './_list.twig' with section %} 32 | {% elseif layout == 'lookbook' %} 33 | {% include './_lookbook.twig' with section %} 34 | {% elseif layout == 'marquee' %} 35 | {% include './_marquee.twig' with section %} 36 | {% elseif layout == 'media' %} 37 | {% include './_media.twig' with section %} 38 | {% elseif layout == 'quote' %} 39 | {% include './_quote.twig' with section %} 40 | {% elseif layout == 'seasons' %} 41 | {% include './_seasons.twig' with section %} 42 | {% elseif layout == 'shop' %} 43 | {% include './_shop.twig' with section %} 44 | {% endif %} 45 | {% endfor %} 46 | -------------------------------------------------------------------------------- /apps/web/views/shared/header.twig: -------------------------------------------------------------------------------- 1 | {# Default #} 2 | 3 | 4 | 5 | 6 | {{ social.title }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% if social.title %} 15 | 16 | 17 | {% endif %} 18 | 19 | {% if social.image %} 20 | {% set asset = getAsset(social.image) %} 21 | 22 | 23 | 24 | {% endif %} 25 | 26 | {% if social.description %} 27 | 28 | 29 | {% endif %} 30 | 31 | {# Stylesheets #} 32 | 33 | 34 | {% if typekit %} 35 | 36 | 37 | 38 | {% endif %} 39 | 40 | 41 | 42 | {# Favicons #} 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /apps/web/views/shared/scripts.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if analytics %} 4 | 5 | 6 | 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lisergia", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "lint": "turbo run lint", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 9 | "check-types": "turbo run check-types" 10 | }, 11 | "devDependencies": { 12 | "prettier": "^3.5.3", 13 | "turbo": "^2.5.2", 14 | "typescript": "5.8.2" 15 | }, 16 | "engines": { 17 | "node": ">=18" 18 | }, 19 | "optionalDependencies": { 20 | "@rollup/rollup-linux-x64-gnu": "4.6.1" 21 | }, 22 | "packageManager": "npm@10.7.0", 23 | "workspaces": [ 24 | "apps/*", 25 | "packages/*" 26 | ], 27 | "dependencies": { 28 | "@zackad/prettier-plugin-twig": "^0.16.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/cli/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@lisergia/config-eslint/index.js'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('dotenv').config() 4 | 5 | const autoprefixer = require('autoprefixer') 6 | const browsersync = require('rollup-plugin-browsersync') 7 | const commonjs = require('@rollup/plugin-commonjs') 8 | const copy = require('rollup-plugin-copy') 9 | const glslify = require('rollup-plugin-glslify') 10 | const { nodeResolve } = require('@rollup/plugin-node-resolve') 11 | const postcss = require('postcss') 12 | const replace = require('@rollup/plugin-replace') 13 | const scss = require('rollup-plugin-scss') 14 | const svg = require('rollup-plugin-svg-icons') 15 | const terser = require('@rollup/plugin-terser') 16 | const typescript = require('@rollup/plugin-typescript') 17 | const { visualizer } = require('rollup-plugin-visualizer') 18 | 19 | const path = require('path') 20 | const rollup = require('rollup') 21 | 22 | const [type] = process.argv.slice(2) 23 | 24 | const root = path.resolve() 25 | 26 | const configuration = { 27 | root: path.join(root), 28 | src: path.join(root, 'src'), 29 | build: path.join(root, 'build'), 30 | } 31 | 32 | const production = type == 'build' 33 | 34 | const config = { 35 | input: path.join(configuration.src, 'app/index.ts'), 36 | output: { 37 | file: path.join(configuration.build, 'bundle.js'), 38 | format: 'cjs', 39 | sourcemap: !production, 40 | }, 41 | plugins: [ 42 | replace({ 43 | preventAssignment: true, 44 | 45 | 'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'), 46 | }), 47 | 48 | commonjs(), 49 | 50 | copy({ 51 | flatten: true, 52 | targets: [ 53 | { 54 | dest: configuration.build, 55 | rename: (name, extension, fullPath) => fullPath.replace(path.join(configuration.src, 'shared'), ''), 56 | src: `${path.join(configuration.src, 'shared')}/**`, 57 | }, 58 | ], 59 | verbose: true, 60 | }), 61 | 62 | glslify({ 63 | compress: production, 64 | }), 65 | 66 | nodeResolve(), 67 | 68 | scss({ 69 | fileName: 'bundle.css', 70 | outputStyle: 'compressed', 71 | processor: () => postcss([autoprefixer()]), 72 | silenceDeprecations: ['color-functions', 'global-builtin', 'import', 'legacy-js-api', 'slash-div'], 73 | watch: path.join(configuration.src, 'styles'), 74 | }), 75 | 76 | svg({ 77 | inputFolder: path.join(configuration.src, 'sprites'), 78 | output: path.join(configuration.build, 'bundle.svg'), 79 | }), 80 | 81 | typescript({ 82 | compilerOptions: { 83 | lib: ['DOM', 'ESNext'], 84 | target: 'ESNext', 85 | }, 86 | include: ['**/*.ts'], 87 | exclude: ['utilities/**/*.ts', 'router/**/*.ts', '*.ts'], 88 | tsconfig: false, 89 | }), 90 | 91 | !production && 92 | browsersync({ 93 | port: process.env.BROWSERSYNC_PORT ?? 3030, 94 | proxy: process.env.BROWSERSYNC_PROXY ?? 'localhost:3000', 95 | files: [ 96 | { 97 | match: [`${path.join(configuration.root, 'views')}/**`], 98 | fn: function (event, file) { 99 | this.reload() 100 | }, 101 | }, 102 | ], 103 | }), 104 | 105 | production && terser(), 106 | 107 | visualizer(), 108 | ], 109 | preserveSymlinks: true, 110 | } 111 | 112 | if (production) { 113 | ;(async () => { 114 | const bundle = await rollup.rollup(config) 115 | 116 | await bundle.write({ 117 | file: config.output.file, 118 | }) 119 | })() 120 | } else { 121 | const watcher = rollup.watch(config) 122 | 123 | watcher.on('event', ({ result }) => { 124 | if (result) { 125 | result.close() 126 | } 127 | }) 128 | 129 | watcher.on('change', (id) => { 130 | console.log('File refreshed', id) 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/cli", 3 | "version": "0.0.0", 4 | "bin": { 5 | "lisergia": "index.js" 6 | }, 7 | "dependencies": { 8 | "@lisergia/config-eslint": "*", 9 | "@lisergia/config-tsconfig": "*", 10 | "@rollup/plugin-commonjs": "^28.0.1", 11 | "@rollup/plugin-node-resolve": "^15.3.0", 12 | "@rollup/plugin-replace": "^6.0.1", 13 | "@rollup/plugin-terser": "^0.4.4", 14 | "@rollup/plugin-typescript": "^12.1.1", 15 | "autoprefixer": "^10.4.20", 16 | "dotenv": "^16.5.0", 17 | "include-media": "^2.0.0", 18 | "postcss": "^8.4.49", 19 | "rollup": "^4.28.1", 20 | "rollup-plugin-browsersync": "^1.3.3", 21 | "rollup-plugin-copy": "^3.5.0", 22 | "rollup-plugin-glslify": "^1.3.1", 23 | "rollup-plugin-scss": "^4.0.0", 24 | "rollup-plugin-svg-icons": "^2.1.2", 25 | "rollup-plugin-typescript2": "^0.36.0", 26 | "rollup-plugin-visualizer": "^5.12.0", 27 | "sass": "^1.82.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/config-eslint/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['./index.js'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/config-eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | ignorePatterns: ['node_modules', 'dist', 'build', 'public', '.next'], 4 | extends: [ 5 | 'airbnb', 6 | 'turbo', 7 | 'plugin:prettier/recommended', 8 | 'plugin:import/typescript', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier', 11 | ], 12 | plugins: ['@typescript-eslint', 'unused-imports', 'prettier', 'simple-import-sort'], 13 | parser: '@typescript-eslint/parser', 14 | rules: { 15 | 'simple-import-sort/exports': 'error', 16 | semi: ['error', 'never'], 17 | quotes: ['error', 'single', { avoidEscape: true }], 18 | 'prettier/prettier': [ 19 | 'error', 20 | { 21 | printWidth: 120, 22 | semi: false, 23 | singleQuote: true, 24 | tabWidth: 2, 25 | trailingComma: 'all', 26 | useTabs: false, 27 | }, 28 | ], 29 | 'padding-line-between-statements': [ 30 | 'error', 31 | { 32 | blankLine: 'always', 33 | prev: '*', 34 | next: ['multiline-const', 'multiline-expression', 'block', 'function', 'if', 'block-like', 'return'], 35 | }, 36 | ], 37 | }, 38 | settings: {}, 39 | } 40 | -------------------------------------------------------------------------------- /packages/config-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/config-eslint", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "devDependencies": { 6 | "@typescript-eslint/eslint-plugin": "5.48.1", 7 | "@typescript-eslint/parser": "5.48.1", 8 | "eslint-config-airbnb": "19.0.4", 9 | "eslint-config-prettier": "8.6.0", 10 | "eslint-config-standard": "17.0.0", 11 | "eslint-config-turbo": "0.0.7", 12 | "eslint-plugin-prettier": "4.2.1", 13 | "eslint-plugin-simple-import-sort": "10.0.0", 14 | "eslint-plugin-unused-imports": "2.0.0", 15 | "eslint": "8.32.0", 16 | "prettier": "2.8.3", 17 | "typescript": "4.9.4" 18 | }, 19 | "scripts": { 20 | "clean": "rm -rf node_modules" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/config-stylelint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-standard-scss'], 3 | plugins: ['stylelint-order', 'stylelint-scss'], 4 | rules: { 5 | 'at-rule-empty-line-before': [ 6 | 'always', 7 | { 8 | ignore: [ 9 | 'after-comment', 10 | 'first-nested', 11 | 'inside-block', 12 | 'blockless-after-same-name-blockless', 13 | 'blockless-after-blockless', 14 | ], 15 | }, 16 | ], 17 | 'custom-property-empty-line-before': null, 18 | 'custom-property-pattern': '[a-z]', 19 | 'no-descending-specificity': null, 20 | 'order/properties-alphabetical-order': true, 21 | 'selector-class-pattern': '[a-z]', 22 | 'scss/no-global-function-names': null, 23 | 'scss/percent-placeholder-pattern': '[a-z]', 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /packages/config-stylelint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/config-stylelint", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "stylelint": "^16.19.1", 7 | "stylelint-config-standard-scss": "^14.0.0", 8 | "stylelint-order": "^7.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/config-tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "allowSyntheticDefaultImports": true, 7 | "composite": false, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "inlineSources": false, 13 | "isolatedModules": true, 14 | "moduleResolution": "node", 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "preserveWatchOutput": true, 18 | "skipLibCheck": true, 19 | "strict": true 20 | }, 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/config-tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/config-tsconfig", 3 | "version": "0.0.0", 4 | "files": [ 5 | "base.json" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/core", 3 | "version": "0.0.0", 4 | "exports": { 5 | "./*": "./src/*.ts" 6 | }, 7 | "main": "./src/index.ts", 8 | "files": [ 9 | "src" 10 | ], 11 | "dependencies": { 12 | "auto-bind": "^5.0.1", 13 | "lenis": "^1.3.1", 14 | "mobx": "^6.13.7", 15 | "nanoevents": "^9.1.0", 16 | "tempus": "1.0.0-dev.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/Component.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationManager } from './App' 2 | import { EventEmitter } from './EventEmitter' 3 | 4 | export interface ComponentClasses { 5 | [key: string]: string 6 | } 7 | 8 | export interface ComponentElements { 9 | [key: string]: Array | Element | Array | HTMLElement | Array | NodeList | Window | null 10 | } 11 | 12 | export type ComponentSelector = string | HTMLElement 13 | 14 | export interface ComponentSelectors { 15 | [key: string]: string | Element | Array | HTMLElement | Array | NodeList | Window 16 | } 17 | 18 | export interface ComponentParameters { 19 | application?: ApplicationManager 20 | autoListeners?: boolean 21 | autoMount?: boolean 22 | classes?: ComponentClasses 23 | element?: ComponentSelector 24 | elements?: ComponentSelectors 25 | id?: string 26 | } 27 | 28 | export class Component extends EventEmitter { 29 | application?: ApplicationManager 30 | autoListeners: boolean 31 | autoMount: boolean 32 | classes?: ComponentClasses 33 | selector?: ComponentSelector 34 | selectors?: ComponentSelectors 35 | 36 | id?: string 37 | 38 | element?: HTMLElement 39 | elements: ComponentElements = {} 40 | 41 | constructor({ 42 | application, 43 | autoListeners = true, 44 | autoMount = true, 45 | classes, 46 | element, 47 | elements, 48 | id, 49 | }: ComponentParameters) { 50 | super() 51 | 52 | this.application = application 53 | this.autoListeners = autoListeners 54 | this.autoMount = autoMount 55 | 56 | this.classes = classes 57 | 58 | this.selector = element 59 | this.selectors = elements 60 | 61 | this.id = id 62 | 63 | if (this.autoMount) { 64 | this.create() 65 | } 66 | 67 | if (this.autoListeners) { 68 | this.addEventListeners() 69 | } 70 | } 71 | 72 | create() { 73 | if (this.selector) { 74 | this.initElement(this.selector) 75 | } 76 | 77 | if (this.selectors) { 78 | this.initElements(this.selectors) 79 | } 80 | } 81 | 82 | initElement(selector: ComponentSelector) { 83 | if (selector instanceof HTMLElement) { 84 | this.element = selector 85 | } else { 86 | this.element = document.querySelector(selector)! 87 | } 88 | } 89 | 90 | initElements(selectors?: ComponentSelectors) { 91 | for (const key in selectors) { 92 | const selector = selectors[key] 93 | 94 | if (selector === window) { 95 | this.elements[key] = window 96 | } else if (selector instanceof HTMLElement) { 97 | this.elements[key] = selector 98 | } else if (selector instanceof NodeList) { 99 | this.elements[key] = selector 100 | } else if (Array.isArray(selector)) { 101 | this.elements[key] = selector 102 | } else { 103 | const elements = this.element!.querySelectorAll(selector as string) 104 | 105 | if (elements.length === 0) { 106 | const elements = document.querySelectorAll(selector as string) 107 | 108 | if (elements.length === 0) { 109 | this.elements[key] = null 110 | } else if (elements.length === 1) { 111 | this.elements[key] = elements[0] as HTMLElement 112 | } else { 113 | this.elements[key] = elements 114 | } 115 | } else if (elements.length === 1) { 116 | this.elements[key] = elements[0] as HTMLElement 117 | } else { 118 | this.elements[key] = elements 119 | } 120 | } 121 | } 122 | } 123 | 124 | addEventListeners() {} 125 | 126 | removeEventListeners() {} 127 | 128 | destroy() { 129 | super.destroy() 130 | 131 | this.removeEventListeners() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /packages/core/src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import AutoBind from 'auto-bind' 2 | import { Emitter, Unsubscribe, createNanoEvents } from 'nanoevents' 3 | 4 | export class EventEmitter { 5 | emitter: Emitter 6 | entries: Map = new Map() 7 | 8 | constructor() { 9 | AutoBind(this) 10 | 11 | this.emitter = createNanoEvents() 12 | } 13 | 14 | on(event: string, callback: (...args: any) => void) { 15 | if (!callback) { 16 | return console.trace('No callback provided') 17 | } 18 | 19 | const emitter = this.emitter.on(event, callback) 20 | 21 | this.entries.set(callback, emitter) 22 | 23 | return emitter 24 | } 25 | 26 | off(event: string, callback: (...args: any) => void) { 27 | const unsubscribe = this.entries.get(callback) 28 | 29 | if (unsubscribe) { 30 | unsubscribe() 31 | } 32 | } 33 | 34 | fire(event: string, ...args: any[]) { 35 | this.emitter.emit(event, ...args) 36 | } 37 | 38 | destroy() { 39 | this.entries.forEach((unsubscribe) => unsubscribe()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/Link.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './Component' 2 | 3 | export class Link extends Component { 4 | declare element: HTMLLinkElement 5 | 6 | constructor({ element }: { element: HTMLAnchorElement }) { 7 | super({ element }) 8 | } 9 | 10 | onClick(event: MouseEvent) { 11 | event.preventDefault() 12 | 13 | this.fire('click', this.element.href) 14 | } 15 | 16 | addEventListeners() { 17 | const isLocal = this.element.href.includes(window.location.origin) 18 | const isLocalPrevented = this.element.dataset.linkOverride === '' 19 | const isNotEmail = !this.element.href.startsWith('mailto') 20 | const isNotPhone = !this.element.href.startsWith('tel') 21 | const isDownload = this.element.hasAttribute('download') 22 | const isAnchor = this.element.href.includes('#') 23 | 24 | if (isAnchor) { 25 | const hash = this.element.href.split('#')[1] 26 | const element = document.querySelector(`#${hash}`) 27 | 28 | if (element) { 29 | return 30 | } 31 | } 32 | 33 | if (isLocalPrevented || isDownload) { 34 | return 35 | } 36 | 37 | if (isLocal) { 38 | this.element.onclick = this.onClick 39 | } else if (isNotEmail && isNotPhone) { 40 | this.element.rel = 'noopener' 41 | this.element.setAttribute('target', '_blank') 42 | } 43 | } 44 | 45 | removeEventListeners() { 46 | this.element.onclick = null 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/Links.ts: -------------------------------------------------------------------------------- 1 | import { reaction } from 'mobx' 2 | 3 | import { ApplicationManager } from './App' 4 | import { Link } from './Link' 5 | import { EventEmitter } from './EventEmitter' 6 | 7 | export class Links extends EventEmitter { 8 | declare application: ApplicationManager 9 | declare links: Array 10 | 11 | constructor(application: ApplicationManager) { 12 | super() 13 | 14 | this.application = application 15 | 16 | reaction( 17 | () => application.currentPage, 18 | () => this.refresh(), 19 | { fireImmediately: true }, 20 | ) 21 | } 22 | 23 | addEventListeners() { 24 | this.links?.forEach((link) => link.destroy()) 25 | 26 | const links = document.querySelectorAll('a') 27 | 28 | this.links = Array.from(links).map((element) => { 29 | const link = new Link({ 30 | element, 31 | }) 32 | 33 | link.on('click', this.onLinkClick) 34 | 35 | return link 36 | }) 37 | } 38 | 39 | onLinkClick(href: string) { 40 | this.application.route = href 41 | } 42 | 43 | refresh() { 44 | this.addEventListeners() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './App' 2 | export * from './Link' 3 | export * from './Links' 4 | export * from './Component' 5 | export * from './EventEmitter' 6 | export * from './Page' 7 | -------------------------------------------------------------------------------- /packages/managers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/managers", 3 | "version": "0.0.0", 4 | "exports": { 5 | "./*": "./src/*.ts" 6 | }, 7 | "main": "./src/index.ts", 8 | "files": [ 9 | "src" 10 | ], 11 | "dependencies": { 12 | "@lisergia/core": "*", 13 | "mobx": "^6.13.7", 14 | "nanoevents": "^9.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/managers/src/Pointer.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@lisergia/core' 2 | 3 | export class PointerManager extends EventEmitter {} 4 | 5 | export const Pointer = new PointerManager() 6 | -------------------------------------------------------------------------------- /packages/managers/src/Viewport.ts: -------------------------------------------------------------------------------- 1 | import { computed, makeObservable, observable } from 'mobx' 2 | import { Unsubscribe } from 'nanoevents' 3 | 4 | import { EventEmitter } from '@lisergia/core' 5 | 6 | export class ViewportManager extends EventEmitter { 7 | static PHONE = 768 8 | static TABLET = 1024 9 | static DESKTOP = 1280 10 | 11 | height: number = window.innerHeight 12 | width: number = window.innerWidth 13 | 14 | entries: Map = new Map() 15 | 16 | constructor() { 17 | super() 18 | 19 | makeObservable(this, { 20 | aspect: computed, 21 | dpr: computed, 22 | 23 | height: observable, 24 | width: observable, 25 | 26 | isPhone: computed, 27 | isTablet: computed, 28 | isDesktop: computed, 29 | }) 30 | 31 | this.onResize() 32 | 33 | window.addEventListener('resize', this.onResize) 34 | } 35 | 36 | get aspect() { 37 | return this.width / this.height 38 | } 39 | 40 | get dpr() { 41 | return window.devicePixelRatio 42 | } 43 | 44 | get isPhone() { 45 | return this.width < ViewportManager.PHONE 46 | } 47 | 48 | get isTablet() { 49 | return this.width >= ViewportManager.PHONE && this.width < ViewportManager.TABLET 50 | } 51 | 52 | get isDesktop() { 53 | return this.width >= ViewportManager.TABLET 54 | } 55 | 56 | on(event: string, callback: (...args: any[]) => void) { 57 | const unsubscribe = super.on(event, callback) 58 | 59 | if (unsubscribe) { 60 | this.entries.set(callback, unsubscribe) 61 | } 62 | 63 | callback(this) 64 | 65 | return unsubscribe 66 | } 67 | 68 | off(event: string, callback: (...args: any[]) => void) { 69 | super.off(event, callback) 70 | } 71 | 72 | onResize() { 73 | this.height = window.innerHeight 74 | this.width = window.innerWidth 75 | 76 | document.documentElement.style.setProperty('--100vh', `${this.height}px`) 77 | 78 | this.fire('resize', this) 79 | } 80 | } 81 | 82 | export const Viewport = new ViewportManager() 83 | -------------------------------------------------------------------------------- /packages/managers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Pointer' 2 | export * from './Viewport' 3 | -------------------------------------------------------------------------------- /packages/styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/styles", 3 | "version": "0.0.0", 4 | "files": [ 5 | "scss" 6 | ], 7 | "scripts": { 8 | "lint": "stylelint \"**/*.scss\"" 9 | }, 10 | "dependencies": { 11 | "@lisergia/config-stylelint": "*", 12 | "include-media": "^2.0.0", 13 | "stylelint": "^16.19.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/styles/scss/base/app.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100%; 3 | overflow: clip; 4 | position: relative; 5 | width: 100vw; 6 | } 7 | -------------------------------------------------------------------------------- /packages/styles/scss/base/lenis.scss: -------------------------------------------------------------------------------- 1 | .lenis { 2 | &:not(.lenis-autoToggle).lenis-stopped { 3 | overflow: clip; 4 | } 5 | 6 | &.lenis-smooth { 7 | [data-lenis-prevent] { 8 | overscroll-behavior: contain; 9 | } 10 | 11 | iframe { 12 | pointer-events: none; 13 | } 14 | } 15 | 16 | &.lenis-autoToggle { 17 | transition-behavior: allow-discrete; 18 | transition-duration: 1ms; 19 | transition-property: overflow; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/styles/scss/base/reset.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body, 9 | div, 10 | span, 11 | applet, 12 | object, 13 | iframe, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | p, 21 | blockquote, 22 | pre, 23 | a, 24 | abbr, 25 | acronym, 26 | address, 27 | big, 28 | cite, 29 | code, 30 | del, 31 | dfn, 32 | em, 33 | img, 34 | ins, 35 | kbd, 36 | q, 37 | s, 38 | samp, 39 | small, 40 | strike, 41 | strong, 42 | sub, 43 | sup, 44 | tt, 45 | var, 46 | b, 47 | u, 48 | i, 49 | center, 50 | dl, 51 | dt, 52 | dd, 53 | ol, 54 | ul, 55 | li, 56 | fieldset, 57 | form, 58 | label, 59 | legend, 60 | table, 61 | caption, 62 | tbody, 63 | tfoot, 64 | thead, 65 | tr, 66 | th, 67 | td, 68 | article, 69 | aside, 70 | canvas, 71 | details, 72 | embed, 73 | figure, 74 | figcaption, 75 | footer, 76 | header, 77 | hgroup, 78 | menu, 79 | nav, 80 | output, 81 | ruby, 82 | section, 83 | summary, 84 | time, 85 | mark, 86 | audio, 87 | video { 88 | border: 0; 89 | font: inherit; 90 | font-size: 100%; 91 | margin: 0; 92 | padding: 0; 93 | vertical-align: baseline; 94 | } 95 | 96 | article, 97 | aside, 98 | details, 99 | figcaption, 100 | figure, 101 | footer, 102 | header, 103 | hgroup, 104 | menu, 105 | nav, 106 | section { 107 | display: block; 108 | } 109 | 110 | html { 111 | -webkit-font-smoothing: antialiased; 112 | -moz-osx-font-smoothing: grayscale; 113 | height: 100%; 114 | left: 0; 115 | position: fixed; 116 | text-rendering: optimizelegibility; 117 | top: 0; 118 | width: 100%; 119 | } 120 | 121 | body { 122 | height: 100%; 123 | left: 0; 124 | line-height: 1; 125 | position: fixed; 126 | top: 0; 127 | width: 100%; 128 | } 129 | 130 | a { 131 | color: inherit; 132 | text-decoration: none; 133 | } 134 | 135 | button, 136 | input, 137 | textarea { 138 | background: none; 139 | border: none; 140 | color: inherit; 141 | font: inherit; 142 | padding: 0; 143 | } 144 | 145 | button { 146 | cursor: pointer; 147 | } 148 | 149 | ol, 150 | ul { 151 | list-style: none; 152 | } 153 | 154 | blockquote, 155 | q { 156 | quotes: none; 157 | } 158 | 159 | blockquote::before, 160 | blockquote::after, 161 | q::before, 162 | q::after { 163 | content: ''; 164 | content: none; 165 | } 166 | 167 | table { 168 | border-collapse: collapse; 169 | border-spacing: 0; 170 | } 171 | -------------------------------------------------------------------------------- /packages/styles/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import './base/app'; 2 | @import './base/reset'; 3 | @import './base/lenis'; 4 | 5 | @import './variables/easings'; 6 | 7 | @import './utils/functions'; 8 | @import './utils/mixins'; 9 | 10 | @import './plugins/include-media'; 11 | -------------------------------------------------------------------------------- /packages/styles/scss/plugins/include-media.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: ( 2 | 'phone': 768px, 3 | 'tablet': 1024px, 4 | 'desktop': 1920px, 5 | ) !default; 6 | 7 | @import '../../node_modules/include-media/dist/include-media'; 8 | -------------------------------------------------------------------------------- /packages/styles/scss/utils/functions.scss: -------------------------------------------------------------------------------- 1 | @function z($name) { 2 | @if index($z-indexes, $name) { 3 | @return (length($z-indexes) - index($z-indexes, $name)) + 1; 4 | } @else { 5 | @warn 'There is no item "#{$name}" in this list; Choose one of: #{$z-indexes}'; 6 | 7 | @return null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/styles/scss/utils/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin hover($element: '') { 2 | @if $element == '' { 3 | html.desktop &:hover { 4 | @content; 5 | } 6 | } @else { 7 | html.desktop #{$element}:hover & { 8 | @content; 9 | } 10 | } 11 | } 12 | 13 | %cover { 14 | height: 100%; 15 | left: 0; 16 | position: absolute; 17 | top: 0; 18 | width: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /packages/styles/scss/variables/easings.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --ease-in-sine: cubic-bezier(0.47, 0, 0.745, 0.715); 3 | --ease-out-sine: cubic-bezier(0.39, 0.575, 0.565, 1); 4 | --ease-in-out-sine: cubic-bezier(0.445, 0.05, 0.55, 0.95); 5 | 6 | --ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53); 7 | --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); 8 | --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955); 9 | 10 | --ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); 11 | --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1); 12 | --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); 13 | 14 | --ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22); 15 | --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1); 16 | --ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1); 17 | 18 | --ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); 19 | --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); 20 | --ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); 21 | 22 | --ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035); 23 | --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); 24 | --ease-in-out-expo: cubic-bezier(1, 0, 0, 1); 25 | 26 | --ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335); 27 | --ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1); 28 | --ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); 29 | 30 | --ease-in-back: cubic-bezier(0.6, -0.28, 0.735, 0.045); 31 | --ease-out-back: cubic-bezier(0.175, 0.885, 0.32, 1.275); 32 | --ease-in-out-back: cubic-bezier(0.68, -0.55, 0.265, 1.55); 33 | } 34 | -------------------------------------------------------------------------------- /packages/styles/stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@lisergia/config-stylelint') 2 | -------------------------------------------------------------------------------- /packages/utilities/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lisergia/utilities", 3 | "version": "0.0.0", 4 | "exports": { 5 | "./*": "./src/*.ts" 6 | }, 7 | "main": "./src/index.ts", 8 | "files": [ 9 | "src" 10 | ], 11 | "dependencies": { 12 | "gsap": "^3.13.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/utilities/src/Canvas.ts: -------------------------------------------------------------------------------- 1 | function trim(canvas: HTMLCanvasElement) { 2 | const context = canvas.getContext('2d')! 3 | 4 | const copy = document.createElement('canvas').getContext('2d', { willReadFrequently: true }) 5 | const pixels = context.getImageData(0, 0, canvas.width, canvas.height) 6 | const length = pixels.data.length 7 | 8 | const bound: { 9 | bottom: number | null 10 | left: number | null 11 | right: number | null 12 | top: number | null 13 | } = { 14 | bottom: null, 15 | left: null, 16 | right: null, 17 | top: null, 18 | } 19 | 20 | let x: number 21 | let y: number 22 | 23 | for (let i = 0; i < length; i += 4) { 24 | if (pixels.data[i + 3] !== 0) { 25 | x = (i / 4) % canvas.width 26 | y = ~~(i / 4 / canvas.width) 27 | 28 | if (bound.top === null) { 29 | bound.top = y 30 | } 31 | 32 | if (bound.left === null) { 33 | bound.left = x 34 | } else if (x < bound.left) { 35 | bound.left = x 36 | } 37 | 38 | if (bound.right === null) { 39 | bound.right = x 40 | } else if (bound.right < x) { 41 | bound.right = x 42 | } 43 | 44 | if (bound.bottom === null) { 45 | bound.bottom = y 46 | } else if (bound.bottom < y) { 47 | bound.bottom = y 48 | } 49 | } 50 | } 51 | 52 | const trimHeight = bound.bottom! - bound.top! 53 | const trimWidth = bound.right! - bound.left! 54 | const trimmed = context.getImageData(bound.left!, bound.top!, trimWidth, trimHeight) 55 | 56 | copy!.canvas.width = trimWidth 57 | copy!.canvas.height = trimHeight 58 | copy!.putImageData(trimmed, 0, 0) 59 | 60 | return copy!.canvas 61 | } 62 | 63 | export const CanvasUtils = { 64 | trim, 65 | } 66 | -------------------------------------------------------------------------------- /packages/utilities/src/DOM.ts: -------------------------------------------------------------------------------- 1 | export interface DOMRectBounds { 2 | bottom: number 3 | height: number 4 | left: number 5 | top: number 6 | width: number 7 | } 8 | 9 | function getBounds(element: HTMLElement, top = 0) { 10 | const box = element.getBoundingClientRect() 11 | 12 | return { 13 | bottom: box.bottom, 14 | height: box.height, 15 | left: box.left, 16 | top: box.top + top, 17 | width: box.width, 18 | } as DOMRectBounds 19 | } 20 | 21 | export const DOMUtils = { 22 | getBounds, 23 | } 24 | -------------------------------------------------------------------------------- /packages/utilities/src/Detection.ts: -------------------------------------------------------------------------------- 1 | export class DetectionManager { 2 | isMobile() { 3 | return !document.documentElement.classList.contains('desktop') 4 | } 5 | } 6 | 7 | export const Detection = new DetectionManager() 8 | -------------------------------------------------------------------------------- /packages/utilities/src/Math.ts: -------------------------------------------------------------------------------- 1 | function lerp(start: number, end: number, time: number) { 2 | return start + (end - start) * time 3 | } 4 | 5 | function clamp(value: number, min: number, max: number) { 6 | return Math.min(Math.max(value, min), max) 7 | } 8 | 9 | function random(min: number, max: number) { 10 | return Math.random() * (max - min) + min 11 | } 12 | 13 | function map( 14 | value: number, 15 | inMin: number, 16 | inMax: number, 17 | outMin: number, 18 | outMax: number, 19 | clamp: boolean = false, 20 | ): number { 21 | let mapped = ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin 22 | 23 | if (clamp) { 24 | const [minOut, maxOut] = outMin < outMax ? [outMin, outMax] : [outMax, outMin] 25 | 26 | mapped = Math.min(Math.max(mapped, minOut), maxOut) 27 | } 28 | 29 | return mapped 30 | } 31 | 32 | export const MathUtils = { 33 | clamp, 34 | lerp, 35 | map, 36 | random, 37 | } 38 | -------------------------------------------------------------------------------- /packages/utilities/src/Polyfill.ts: -------------------------------------------------------------------------------- 1 | declare interface HTMLElement { 2 | forEach: Function 3 | } 4 | 5 | declare interface NodeList { 6 | filter: Function 7 | find: Function 8 | map: Function 9 | } 10 | 11 | const HTMLElementPrototype = HTMLElement.prototype as { 12 | forEach: Function 13 | } 14 | 15 | const NodeListPrototype = NodeList.prototype as { 16 | filter: Function 17 | find: Function 18 | map: Function 19 | } 20 | 21 | /** 22 | * Allow `forEach` to work with single HTMLElement. 23 | */ 24 | if (!HTMLElementPrototype.forEach) { 25 | HTMLElementPrototype.forEach = function (callback: Function, thisArg: any) { 26 | thisArg = thisArg || window 27 | 28 | callback.call(thisArg, this, this, this) 29 | } 30 | } 31 | 32 | /** 33 | * Allow `filter` to work with NodeList. 34 | */ 35 | if (!NodeListPrototype.filter) { 36 | NodeListPrototype.filter = Array.prototype.filter 37 | } 38 | 39 | /** 40 | * Allow `find` to work with NodeList. 41 | */ 42 | if (!NodeListPrototype.find) { 43 | NodeListPrototype.find = Array.prototype.find 44 | } 45 | 46 | /** 47 | * Allow `map` to work with NodeList. 48 | */ 49 | if (!NodeListPrototype.map) { 50 | NodeListPrototype.map = Array.prototype.map 51 | } 52 | -------------------------------------------------------------------------------- /packages/utilities/src/Promises.ts: -------------------------------------------------------------------------------- 1 | export const createPromise = (): [Promise, (value: T) => void] => { 2 | let resolve: (value: T) => void 3 | 4 | const promise = new Promise((resolvePromise) => { 5 | resolve = resolvePromise 6 | }) 7 | 8 | return [promise, resolve!] 9 | } 10 | 11 | export const createPromiseWithReject = (): [Promise, (value: T) => void, (reason?: any) => void] => { 12 | let resolve: (value: T) => void 13 | let reject: (reason?: any) => void 14 | 15 | const promise = new Promise((resolvePromise, rejectPromise) => { 16 | resolve = resolvePromise 17 | reject = rejectPromise 18 | }) 19 | 20 | return [promise, resolve!, reject!] 21 | } 22 | 23 | export const createPromiseWithTimeout = ( 24 | timeout: number, 25 | ): [Promise, (value: T) => void, (reason?: any) => void] => { 26 | let resolve: (value: T) => void 27 | let reject: (reason?: any) => void 28 | 29 | const promise = new Promise((resolvePromise, rejectPromise) => { 30 | resolve = resolvePromise 31 | reject = rejectPromise 32 | }) 33 | 34 | const timer = setTimeout(() => { 35 | reject(new Error('Promise timed out')) 36 | }, timeout) 37 | 38 | promise.finally(() => { 39 | clearTimeout(timer) 40 | }) 41 | 42 | return [promise, resolve!, reject!] 43 | } 44 | -------------------------------------------------------------------------------- /packages/utilities/src/Text.ts: -------------------------------------------------------------------------------- 1 | export * from 'gsap/src/SplitText' 2 | -------------------------------------------------------------------------------- /packages/utilities/src/index.ts: -------------------------------------------------------------------------------- 1 | import './Polyfill' 2 | 3 | export * from './Canvas' 4 | export * from './Detection' 5 | export * from './DOM' 6 | export * from './Math' 7 | export * from './Text' 8 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.com/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "env": ["GOOGLE_ANALYTICS", "SANITY_API", "SANITY_DATABASE", "SANITY_PROJECT"], 8 | "inputs": ["$TURBO_DEFAULT$", ".env*"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "check-types": { 14 | "dependsOn": ["^check-types"] 15 | }, 16 | "dev": { 17 | "dependsOn": ["^build"], 18 | "cache": false, 19 | "persistent": true 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------