first commit
This commit is contained in:
commit
7d704814f5
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Nuxt 3 Minimal Starter
|
||||
|
||||
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm run dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm run build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm run preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
274
app.vue
Normal file
274
app.vue
Normal file
@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-10 bg-white dark:bg-BgDarkSecond p-10">
|
||||
<div
|
||||
class="flex items-center justify-center gap-20 relative w-[1200px] mx-auto"
|
||||
>
|
||||
<!-- MlButton: This button can be used for various actions like form submissions or dialog openings. Customize its appearance using the :pt prop. -->
|
||||
<MlButton
|
||||
:pt="{
|
||||
root: { class: '' },
|
||||
outer: { class: 'bg-gradient-to-r from-accent1 to-accent2' },
|
||||
inner: { class: 'dark:bg-gradient-to-r bg-white' },
|
||||
}"
|
||||
>
|
||||
Let's talk
|
||||
</MlButton>
|
||||
|
||||
<!-- MlPaginator: Handles item pagination. Customize the appearance and behavior via props. Listens to changes with @update:paginatedItems to manage displayed items. -->
|
||||
<MlPaginator
|
||||
:items="items"
|
||||
:items-per-page="2"
|
||||
:max-pages-shown="2"
|
||||
@update:paginatedItems="handlePaginatedItems"
|
||||
v-model="currentPage"
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'flex space-x-1 bg-white/85 dark:bg-BgDark/85 shadow-custom-lrb items-center px-3 py-2 rounded-full dark:text-white',
|
||||
},
|
||||
paginator: { class: 'bg-red-500' },
|
||||
activePaginator: { class: 'bg-accent2 dark:bg-accent1' },
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- MlDropDown: A customizable dropdown menu component for selecting options.
|
||||
Use the :pt prop to define styles for the root, dropdown, title, and arrow sections.
|
||||
The dropdown supports multiple items which can be styled individually using the :pt prop for each MlDropDownItem. -->
|
||||
|
||||
<MlDropDown
|
||||
title="Dropdown"
|
||||
:title="''"
|
||||
:pt="{
|
||||
root: { class: 'w-10 h-10 text-left absolute left-0 z-0' },
|
||||
dropdown: {
|
||||
class:
|
||||
'w-[150px] right-[-50px] absolute mt-4 shadow-custom-lrb dark:shadow-custom-drb block',
|
||||
},
|
||||
title: { class: 'w-[30px] h-10' },
|
||||
arrow: { class: 'hidden' },
|
||||
}"
|
||||
>
|
||||
<!-- Each MlDropDownItem represents an option in the dropdown menu. Style each item and define click behavior for interactivity. -->
|
||||
<MlDropDownItem :pt="{ root: { class: 'block px-1 py-3' } }">
|
||||
<span
|
||||
class="w-full cursor-pointer font-medium flex items-center justify-center"
|
||||
>English</span
|
||||
>
|
||||
</MlDropDownItem>
|
||||
<MlDropDownItem :pt="{ root: { class: 'block px-1 py-3' } }">
|
||||
<span
|
||||
class="w-full cursor-pointer font-medium flex items-center justify-center"
|
||||
>Polis</span
|
||||
>
|
||||
</MlDropDownItem>
|
||||
</MlDropDown>
|
||||
|
||||
<!-- MlThemeSwitch: A button for toggling between light and dark themes. Does not require additional props for basic functionality. -->
|
||||
<MlThemeSwitch />
|
||||
</div>
|
||||
|
||||
<!-- MlSlider: Displays a series of items as a slider. Use the :pt prop to style it and the default slot to customize the content of each slide. -->
|
||||
<MlSlider
|
||||
:items="items"
|
||||
:direction="'horizontal'"
|
||||
:pt="{
|
||||
root: { class: '' },
|
||||
slider: { class: '' },
|
||||
counter: { class: '' },
|
||||
}"
|
||||
class="my-[50px] relative z-30"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
class="bg-white/85 dark:bg-BgDark/85 backdrop-blur-md mx-auto my-4 rounded-3xl w-80 md:w-[800px] shadow-custom-lrb text-black dark:text-white"
|
||||
>
|
||||
<div class="flex justify-between items-center px-8 py-2 border-b-2">
|
||||
<p class="font-semibold text-[12px] uppercase tracking-widest">
|
||||
{{ item.title }}
|
||||
</p>
|
||||
<div
|
||||
class="bg-BgDark dark:bg-BgLight rounded-lg w-5 h-0.5 cursor-pointer"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="flex md:flex-row-reverse flex-col gap-8 py-12 md:py-8 md:w-[800px]"
|
||||
>
|
||||
<div class="flex justify-center w-full">
|
||||
<!-- Placeholder for image content -->
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 mx-8">
|
||||
<h1 class="font-bold text-2xl md:text-[32px]">
|
||||
{{ item.title }}
|
||||
</h1>
|
||||
<p class="font-ibm text-lg">
|
||||
{{ item.info }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MlSlider>
|
||||
|
||||
<!-- MlSliderPoint: Similar to MlSlider but intended for full-page content viewing. Customize its layout and appearance using the :pt prop and slots. -->
|
||||
<div class="flex w-full justify-center text-black dark:text-white">
|
||||
<MlSliderPoint
|
||||
:direction="direction"
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'px-5 w-full flex items-center justify-center flex-col md:max-w-[1200px] lg:flex-row md:justify-between md:gap-20 lg:gap-0',
|
||||
},
|
||||
slider: { class: 'w-full h-full md:max-h-[470px]' },
|
||||
counter: { class: 'lg:rotate-90 z-10 w-[220px] md:w-[280px]' },
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="opacity-90 mx-auto rounded-3xl w-80 md:max-w-[800px] md:w-full h-full"
|
||||
>
|
||||
<div
|
||||
class="flex md:flex-row flex-col items-center gap-8 py-12 md:py-0 w-full h-full"
|
||||
>
|
||||
<div class="w-full">
|
||||
<!-- Placeholder for image content -->
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<h1 class="font-bold text-2xl md:text-[32px]">YourGold.cz</h1>
|
||||
<div class="flex items-center gap-2 font-ibm">
|
||||
<p class="bg-accent2 dark:bg-accent1 px-3 py-1 rounded-md">
|
||||
Tag one
|
||||
</p>
|
||||
<p class="bg-accent2 dark:bg-accent1 px-3 py-1 rounded-md">
|
||||
Tag two
|
||||
</p>
|
||||
<p class="bg-accent2 dark:bg-accent1 px-3 py-1 rounded-md">
|
||||
Tag three
|
||||
</p>
|
||||
</div>
|
||||
<p class="font-ibm text-[16px]">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Suspendisse varius enim in eros elementum tristique. Duis
|
||||
cursus, mi quis viverra ornare, eros dolor interdum nulla, ut
|
||||
commodo diam libero vitae erat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MlSliderPoint>
|
||||
</div>
|
||||
|
||||
<!-- Tabber Component: Use this for tabbed data display. Bind the tabs data structure to manage different tab content dynamically. -->
|
||||
<div
|
||||
class="max-w-[1200px] mx-auto border-[1.5px] border-gray-400 rounded-2xl text-black dark:text-white"
|
||||
>
|
||||
<MlTabberBase :tabs="tabs" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Setup script for handling paginator and tab data.
|
||||
const currentItems = ref([]);
|
||||
const currentPage = ref(1);
|
||||
const handlePaginatedItems = (items) => {
|
||||
currentItems.value = items;
|
||||
};
|
||||
// Array of items to be displayed and managed by MlPaginator.
|
||||
const items = [
|
||||
{
|
||||
id: 1,
|
||||
title:
|
||||
"How to enforce conventional commits using git-cliff with a CI pipeline?",
|
||||
date: "01.07.2024",
|
||||
persone1: "Natalia Goc",
|
||||
persone2: "Marek Goc",
|
||||
info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Understanding JavaScript Promises",
|
||||
date: "02.07.2024",
|
||||
persone1: "John Doe",
|
||||
persone2: "Jane Smith",
|
||||
info: "JavaScript promises are used to handle asynchronous operations. They provide an alternative approach to callbacks by returning a value in the future.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "An Introduction to WebAssembly",
|
||||
date: "03.07.2024",
|
||||
persone1: "Alice Johnson",
|
||||
persone2: "Bob Brown",
|
||||
info: "WebAssembly is a binary instruction format for a stack-based virtual machine, designed as a portable target for compilation of high-level languages like C, C++, and Rust.",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Building Responsive Web Applications",
|
||||
date: "04.07.2024",
|
||||
persone1: "Emily White",
|
||||
persone2: "Chris Green",
|
||||
info: "Responsive web design ensures that web applications work well on a variety of devices, providing an optimal user experience across different screen sizes.",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Exploring the Basics of Machine Learning",
|
||||
date: "05.07.2024",
|
||||
persone1: "Michael Lee",
|
||||
persone2: "Sara Kim",
|
||||
info: "Machine learning is a field of artificial intelligence that uses statistical techniques to give computer systems the ability to learn from data.",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Getting Started with Docker",
|
||||
date: "06.07.2024",
|
||||
persone1: "David Brown",
|
||||
persone2: "Laura Black",
|
||||
info: "Docker is a tool designed to make it easier to create, deploy, and run applications by using containers, which allow a developer to package up an application with all of its parts.",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Introduction to Kubernetes",
|
||||
date: "07.07.2024",
|
||||
persone1: "James Wilson",
|
||||
persone2: "Olivia Martin",
|
||||
info: "Kubernetes is an open-source platform designed to automate deploying, scaling, and operating application containers, providing a framework to run distributed systems resiliently.",
|
||||
},
|
||||
];
|
||||
|
||||
// Array of tabber to be displayed and managed by MlTabberBase.
|
||||
const tabs = [
|
||||
{
|
||||
header: "Web App Development",
|
||||
content: {
|
||||
title: "Web App Development",
|
||||
info: "From simple business portfolios to complex interactive web applications, we can realize almost any service available through the Web. We will take care of the aesthetics, accessibility, stability and performance of your web application.",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "E-commerce",
|
||||
content: {
|
||||
title: "Mobile App Development",
|
||||
info: "E-commerce services have became a fundamental element of many well prospering businesses. Having our own experience with it we will be able to help you with the high expectations of your online customers.",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Business Management and ERP Systems",
|
||||
content: {
|
||||
title: "Digital Marketing",
|
||||
info: "Well designed IT infrastructure can be a key element in your company’s success. With us you will discover how modern software can help you manage your business.",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Hosting and IT Infrastructure",
|
||||
content: {
|
||||
title: "Web App Development",
|
||||
info: "We are ready to fill any gaps in your infrastructure. Working with us you won’t have to worry about managing complex costs or hardware resources.",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "IT Consulting",
|
||||
content: {
|
||||
title: "IT Consulting",
|
||||
info: "We are happy to share our experiences with other companies. Besides free consulting during project cost evaluation we offer our knowledge as an independent and affordable service.",
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
53
components/Ml/Accordion/MlAccordionBase.vue
Normal file
53
components/Ml/Accordion/MlAccordionBase.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { AccordionProps, tAccordionStates } from './types';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const props = withDefaults(defineProps<AccordionProps>(), {
|
||||
alwaysOpen: true,
|
||||
openFirstItem: true,
|
||||
flush: false,
|
||||
mode: 'single'
|
||||
});
|
||||
|
||||
const classes = computed(() => twMerge('', props.class));
|
||||
|
||||
const state = ref({
|
||||
options: { ...props },
|
||||
panels: {},
|
||||
initialized: false
|
||||
} as tAccordionStates);
|
||||
|
||||
const openCloseEvent = (panelId: number): void => {
|
||||
let isanyopened = false;
|
||||
if (props.mode == 'single') {
|
||||
for (const i in state.value.panels) {
|
||||
if (parseInt(i) == panelId) {
|
||||
state.value.panels[i].isOpen = !state.value.panels[i].isOpen;
|
||||
} else {
|
||||
state.value.panels[i].isOpen = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.value.panels[panelId].isOpen = !state.value.panels[panelId].isOpen;
|
||||
}
|
||||
if (props.alwaysOpen) {
|
||||
for (const i in state.value.panels) {
|
||||
if (state.value.panels[i].isOpen) {
|
||||
isanyopened = true;
|
||||
}
|
||||
}
|
||||
if (!isanyopened) {
|
||||
state.value.panels[panelId].isOpen = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
provide('accordionState', state);
|
||||
provide('openCloseEvent', openCloseEvent);
|
||||
</script>
|
30
components/Ml/Accordion/MlAccordionContent.vue
Normal file
30
components/Ml/Accordion/MlAccordionContent.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div v-show="isOpen" :class="contentClasses">
|
||||
<!-- <MaalTransitionFade :delay="0" :duration="500"> -->
|
||||
<slot />
|
||||
<!-- </MaalTransitionFade> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { tAccordionStates } from './types';
|
||||
|
||||
const panelId = inject('panelId') as Ref<number>;
|
||||
if (panelId == undefined) {
|
||||
throw {
|
||||
message: 'Looks that you are trying to use component AccordionContent without AccordionPanel as parent',
|
||||
statusCode: 507
|
||||
};
|
||||
}
|
||||
|
||||
const accordionState = inject('accordionState') as Ref<tAccordionStates>;
|
||||
if (accordionState == undefined) {
|
||||
throw {
|
||||
message: 'Looks that you are trying to use component AccordionContent without AccordionPanel as parent',
|
||||
statusCode: 507
|
||||
};
|
||||
}
|
||||
const contentClasses = 'p-6';
|
||||
|
||||
const isOpen = computed(() => accordionState.value.panels[panelId.value].isOpen);
|
||||
</script>
|
50
components/Ml/Accordion/MlAccordionHeader.vue
Normal file
50
components/Ml/Accordion/MlAccordionHeader.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div :class="wrapperClasses">
|
||||
<button
|
||||
type="button"
|
||||
:class="[headerClasses]"
|
||||
@click="
|
||||
useRipple($event, props.ripple);
|
||||
openCloseEvent(panelId);
|
||||
"
|
||||
>
|
||||
<span class="w-full">
|
||||
<slot />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { tAccordionStates, AccordionHeaderProps } from './types';
|
||||
|
||||
const panelId = inject('panelId') as Ref<number>;
|
||||
if (panelId == undefined) {
|
||||
throw {
|
||||
message: 'Looks that you are trying to use component AccordionHeader without AccordionPanel as parent',
|
||||
statusCode: 507
|
||||
};
|
||||
}
|
||||
const accordionState = inject('accordionState') as Ref<tAccordionStates>;
|
||||
if (accordionState == undefined) {
|
||||
throw {
|
||||
message: 'Looks that you are trying to use component AccordionHeader without AccordionPanel as parent',
|
||||
statusCode: 507
|
||||
};
|
||||
}
|
||||
|
||||
const openCloseEvent = inject('openCloseEvent') as Function;
|
||||
if (openCloseEvent == undefined) {
|
||||
throw {
|
||||
message: 'Looks that you are trying to use component AccordionHeader without AccordionPanel or Accordion as parent',
|
||||
statusCode: 507
|
||||
};
|
||||
}
|
||||
|
||||
const headerClasses = `flex items-center justify-between w-full px-6 py-4 font-medium rtl:text-right gap-3 text-left`;
|
||||
const wrapperClasses = '';
|
||||
|
||||
const props = withDefaults(defineProps<AccordionHeaderProps>(), {
|
||||
ripple: true
|
||||
});
|
||||
</script>
|
32
components/Ml/Accordion/MlAccordionPanel.vue
Normal file
32
components/Ml/Accordion/MlAccordionPanel.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="md:flex last:border-b-0">
|
||||
<slot name="header" />
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { AccordionPanelProps, tAccordionStates } from './types';
|
||||
|
||||
withDefaults(defineProps<AccordionPanelProps>(), {});
|
||||
|
||||
const accordionState = inject('accordionState') as Ref<tAccordionStates>;
|
||||
if (accordionState == undefined) {
|
||||
throw {
|
||||
message: 'Looks that you are trying to use component AccordionPanel without Accordion as parent',
|
||||
statusCode: 507
|
||||
};
|
||||
}
|
||||
const panelId = ref(Object.keys(accordionState.value.panels).length);
|
||||
// inject state of this panel
|
||||
accordionState.value.panels[panelId.value] = {
|
||||
isOpen: ((): boolean => {
|
||||
if (accordionState.value.options.openFirstItem && panelId.value == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})()
|
||||
};
|
||||
|
||||
provide('panelId', panelId);
|
||||
</script>
|
41
components/Ml/Accordion/types.ts
Normal file
41
components/Ml/Accordion/types.ts
Normal file
@ -0,0 +1,41 @@
|
||||
export type tAccordionMode = 'flush' | 'alwaysOpen' | 'default';
|
||||
|
||||
export type tAccordionPanel = {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
type tAccordionPanels = {
|
||||
[key: string]: tAccordionPanel;
|
||||
};
|
||||
|
||||
type tStateElement = {
|
||||
id: string;
|
||||
flush: boolean;
|
||||
alwaysOpen: boolean;
|
||||
openFirstItem: boolean;
|
||||
panels: tAccordionPanels;
|
||||
};
|
||||
|
||||
export type tState = {
|
||||
[key: string]: tStateElement;
|
||||
};
|
||||
//////////////
|
||||
|
||||
export interface AccordionHeaderProps {
|
||||
ripple?: boolean;
|
||||
}
|
||||
|
||||
export interface AccordionProps {
|
||||
openFirstItem?: boolean;
|
||||
alwaysOpen?: boolean;
|
||||
mode?: 'single' | 'multi';
|
||||
class?: string | string[];
|
||||
}
|
||||
|
||||
export type tAccordionStates = {
|
||||
options: AccordionProps;
|
||||
initialized: boolean;
|
||||
panels: { [key: number]: tAccordionPanel };
|
||||
};
|
||||
|
||||
export type AccordionPanelProps = {};
|
39
components/Ml/Button/MlButton.vue
Normal file
39
components/Ml/Button/MlButton.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<button :class="rootClasses" @click="useRipple($event, true)">
|
||||
<div :class="outerClasses">
|
||||
<div :class="innerClasses">
|
||||
<ml-spinner v-if="spinerPlace == 'prefix' && props.showSpiner" :color="props.spinerColor" :size="props.spinerSize" class="mx-2 fill-accent1" />
|
||||
<slot></slot>
|
||||
<ml-spinner v-if="spinerPlace == 'suffix' && props.showSpiner" :color="props.spinerColor" :size="props.spinerSize" class="mx-2 fill-accent1" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ButtonProps, PTAttribs } from './types';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
rounded: true,
|
||||
spinerSize: '6',
|
||||
spinerColor: 'accent1',
|
||||
spinerPlace: 'prefix'
|
||||
});
|
||||
|
||||
const classes = ref({
|
||||
root: {
|
||||
class: `flex justify-center items-center disabled:opacity-60`
|
||||
},
|
||||
outer: {
|
||||
class: `bg-gradient-to-r from-accent1 to-accent2 p-[2px] w-full focus:outline-none ${props.rounded ? 'rounded-3xl' : ''}`
|
||||
},
|
||||
inner: {
|
||||
class: `justify-center flex items-center bg-white dark:bg-gradient-to-r px-5 py-2 w-full h-full font-semibold leading-[1.5] back ${props.rounded ? 'rounded-3xl' : ''}`
|
||||
}
|
||||
} as PTAttribs);
|
||||
|
||||
const rootClasses = computed(() => twMerge(classes.value.root?.class, props.pt?.root?.class));
|
||||
const innerClasses = computed(() => twMerge(classes.value.inner?.class, props.pt?.inner?.class));
|
||||
const outerClasses = computed(() => twMerge(classes.value.outer?.class, props.pt?.outer?.class));
|
||||
</script>
|
24
components/Ml/Button/types.ts
Normal file
24
components/Ml/Button/types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { SpinnerSize, SpinnerColor } from '../Spinner/types';
|
||||
|
||||
export type ButtonProps = {
|
||||
rounded?: boolean;
|
||||
pt?: PTAttribs;
|
||||
spinerSize?: SpinnerSize;
|
||||
spinerColor?: SpinnerColor;
|
||||
spinerPlace?: spinerPlace;
|
||||
showSpiner?: boolean;
|
||||
};
|
||||
|
||||
export type PTAttribs = {
|
||||
root?: {
|
||||
class: string;
|
||||
};
|
||||
outer?: {
|
||||
class: string;
|
||||
};
|
||||
inner?: {
|
||||
class: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type spinerPlace = 'suffix' | 'prefix';
|
62
components/Ml/DropDown/MlDropDown.vue
Normal file
62
components/Ml/DropDown/MlDropDown.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div v-click-outside="closeDropDown" :class="rootPT">
|
||||
<button
|
||||
id="dropdownDefaultButton"
|
||||
data-dropdown-toggle="dropdown"
|
||||
:class="titlePT"
|
||||
type="button"
|
||||
@click="
|
||||
useRipple($event, props.ripple);
|
||||
openMenu();
|
||||
"
|
||||
>
|
||||
{{ props.title }}
|
||||
<span :class="arrowPT">
|
||||
<ml-arrow></ml-arrow>
|
||||
</span>
|
||||
</button>
|
||||
<div v-show="isMenuOpen" :class="dropdownPT" @click="isOpen = false">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DropDownProps, DropDownPT } from './types';
|
||||
|
||||
const props = withDefaults(defineProps<DropDownProps>(), {
|
||||
isOpen: false
|
||||
});
|
||||
|
||||
const PT = {
|
||||
root: {
|
||||
class: 'relative pointer'
|
||||
},
|
||||
dropdown: {
|
||||
class: 'z-10 absolute bg-white dark:bg-BgDark/85 rounded-3xl pointer text-black dark:text-white text-center'
|
||||
},
|
||||
title: {
|
||||
class: 'text-black dark:text-BgLight inline-flex items-center rounded-lg font-medium text-center text-base pointer '
|
||||
},
|
||||
arrow: {
|
||||
class: 'px-2'
|
||||
}
|
||||
} as DropDownPT;
|
||||
|
||||
const rootPT = computed(() => twMerge(PT.root?.class, props.pt?.root?.class));
|
||||
const titlePT = computed(() => twMerge(PT.title?.class, props.pt?.title?.class));
|
||||
const dropdownPT = computed(() => twMerge(PT.dropdown?.class, props.pt?.dropdown?.class));
|
||||
const arrowPT = computed(() => twMerge(PT.arrow?.class, props.pt?.arrow?.class));
|
||||
|
||||
const isOpen = ref(props.isOpen);
|
||||
|
||||
function openMenu() {
|
||||
isOpen.value = !isOpen.value;
|
||||
}
|
||||
|
||||
function closeDropDown() {
|
||||
isOpen.value = false;
|
||||
}
|
||||
|
||||
const isMenuOpen = computed(() => isOpen.value);
|
||||
</script>
|
20
components/Ml/DropDown/MlDropDownItem.vue
Normal file
20
components/Ml/DropDown/MlDropDownItem.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div :class="rootPT">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DropDownItemPT, DropDownItemProps } from './types';
|
||||
|
||||
const props = withDefaults(defineProps<DropDownItemProps>(), {});
|
||||
|
||||
const PT = {
|
||||
root: {
|
||||
class: ''
|
||||
}
|
||||
} as DropDownItemPT;
|
||||
|
||||
const rootPT = computed(() => twMerge(PT.root?.class, props.pt?.root?.class));
|
||||
</script>
|
21
components/Ml/DropDown/types.ts
Normal file
21
components/Ml/DropDown/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export type DropDownProps = {
|
||||
pt?: DropDownPT;
|
||||
isOpen?: boolean;
|
||||
title: string;
|
||||
ripple?: boolean;
|
||||
};
|
||||
|
||||
export type DropDownPT = {
|
||||
root?: { class: string };
|
||||
title?: { class: string };
|
||||
dropdown?: { class: string };
|
||||
arrow?: { class: string };
|
||||
};
|
||||
|
||||
export type DropDownItemProps = {
|
||||
pt?: DropDownItemPT;
|
||||
};
|
||||
|
||||
export type DropDownItemPT = {
|
||||
root?: { class: string };
|
||||
};
|
68
components/Ml/Modal/MlModal.vue
Normal file
68
components/Ml/Modal/MlModal.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="modelValue"
|
||||
v-block-scroll="modelValue"
|
||||
class="top-0 left-0 z-[100] fixed flex justify-center items-center bg-gray-900 opacity-80 w-screen h-screen"
|
||||
@click.self="emitClose"
|
||||
>
|
||||
<div class="bg-gray-100 dark:bg-gray-700 border rounded w-1/2">
|
||||
<div class="flex justify-between p-2 border-b">
|
||||
<h2 class="pr-4 font-bold">Header title</h2>
|
||||
<button class="border" @click="emitClose">
|
||||
<XMarkIcon class="size-6"></XMarkIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<slot>
|
||||
<RocketLaunchIcon class="float-left mr-4 text-green-600 size-20"></RocketLaunchIcon>
|
||||
<p>content</p>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RocketLaunchIcon, XMarkIcon } from '@heroicons/vue/24/solid';
|
||||
import type { ModalProps } from './types';
|
||||
const props = withDefaults(defineProps<ModalProps>(), {});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', closeModalKeyBoardEvent);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', closeModalKeyBoardEvent);
|
||||
});
|
||||
|
||||
function closeModalKeyBoardEvent(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
emitClose();
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(name: 'update:modelValue', info: boolean): void;
|
||||
}>();
|
||||
|
||||
function emitClose() {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active {
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.fade-leave-active {
|
||||
transition: all 0.4s ease-in;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
3
components/Ml/Modal/types.ts
Normal file
3
components/Ml/Modal/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type ModalProps = {
|
||||
modelValue?: boolean;
|
||||
};
|
524
components/Ml/Paginator/MlPaginator.vue
Normal file
524
components/Ml/Paginator/MlPaginator.vue
Normal file
@ -0,0 +1,524 @@
|
||||
<script setup lang="ts">
|
||||
import { toRef, type PropType, computed, ref, watch, onMounted, defineProps, defineEmits } from 'vue';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { PTAttribs } from './types';
|
||||
|
||||
const props = defineProps({
|
||||
pt: {
|
||||
type: Object,
|
||||
default: function () {}
|
||||
},
|
||||
items: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
validator: (value: number) => value > 0 || 'itemsPerPage must be greater than 0.'
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: true,
|
||||
validator: (value: number) => value > 0 || 'v-model must be greater than 0.'
|
||||
},
|
||||
maxPagesShown: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
validator: (value: number) => value > 0 || 'maxPagesShown must be greater than 0.'
|
||||
},
|
||||
dir: {
|
||||
type: String as PropType<'ltr' | 'rtl'>,
|
||||
default: 'ltr',
|
||||
validator: (value: string) => ['ltr', 'rtl'].includes(value) || 'dir must be either "ltr" or "rtl".'
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'link' | 'button'>,
|
||||
default: 'button',
|
||||
validator: (value: string) => ['link', 'button'].includes(value) || 'type must be "link" or "button".'
|
||||
},
|
||||
onClick: Function,
|
||||
locale: {
|
||||
type: String as PropType<'en' | 'ar' | 'ir'>,
|
||||
default: 'en',
|
||||
validator: (value: string) => ['en', 'ar', 'ir'].includes(value) || 'locale must be "en", "ar", or "ir".'
|
||||
},
|
||||
prevButtonContent: {
|
||||
type: String,
|
||||
default: '<'
|
||||
},
|
||||
nextButtonContent: {
|
||||
type: String,
|
||||
default: '>'
|
||||
},
|
||||
hidePrevNext: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hidePrevNextWhenEnds: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showBreakpointButtons: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disableBreakpointButtons: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
startingBreakpointContent: {
|
||||
type: String,
|
||||
default: '...'
|
||||
},
|
||||
endingBreakpointButtonContent: {
|
||||
type: String,
|
||||
default: '...'
|
||||
},
|
||||
showJumpButtons: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
linkUrl: {
|
||||
type: String,
|
||||
default: '#'
|
||||
},
|
||||
backwardJumpButtonContent: {
|
||||
type: String,
|
||||
default: '<<'
|
||||
},
|
||||
forwardJumpButtonContent: {
|
||||
type: String,
|
||||
default: '>>'
|
||||
},
|
||||
disablePagination: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showEndingButtons: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
firstPageContent: {
|
||||
type: String,
|
||||
default: 'First'
|
||||
},
|
||||
lastPageContent: {
|
||||
type: String,
|
||||
default: 'Last'
|
||||
},
|
||||
|
||||
// Class props
|
||||
backButtonClass: String,
|
||||
nextButtonClass: {
|
||||
type: String,
|
||||
default: 'next-button'
|
||||
},
|
||||
firstButtonClass: {
|
||||
type: String,
|
||||
default: 'first-button'
|
||||
},
|
||||
lastButtonClass: {
|
||||
type: String,
|
||||
default: 'last-button'
|
||||
},
|
||||
numberButtonsClass: {
|
||||
type: String,
|
||||
default: 'number-buttons'
|
||||
},
|
||||
startingBreakpointButtonClass: {
|
||||
type: String,
|
||||
default: 'starting-breakpoint-button'
|
||||
},
|
||||
endingBreakPointButtonClass: {
|
||||
type: String,
|
||||
default: 'ending-breakpoint-button'
|
||||
},
|
||||
firstPageButtonClass: {
|
||||
type: String,
|
||||
default: 'first-page-button'
|
||||
},
|
||||
lastPageButtonClass: {
|
||||
type: String,
|
||||
default: 'last-page-button'
|
||||
},
|
||||
paginateButtonsClass: {
|
||||
type: String,
|
||||
default: 'px-3 py-1 rounded-full font-[1000] text-sm'
|
||||
},
|
||||
disabledPaginateButtonsClass: {
|
||||
type: String,
|
||||
default: 'disabled-paginate-buttons'
|
||||
},
|
||||
disabledBreakPointButtonClass: String,
|
||||
backwardJumpButtonClass: String,
|
||||
forwardJumpButtonClass: String,
|
||||
disabledBackwardJumpButtonClass: String,
|
||||
disabledBackButtonClass: String,
|
||||
disabledFirstButtonClass: String,
|
||||
disabledLastButtonClass: String,
|
||||
disabledNextButtonClass: String,
|
||||
disabledForwardJumpButtonClass: String
|
||||
});
|
||||
|
||||
const classes = ref({
|
||||
root: { class: `` },
|
||||
paginator: { class: `` },
|
||||
activePaginator: { class: '' }
|
||||
} as PTAttribs);
|
||||
|
||||
const rootClasses = computed(() => twMerge(classes.value.root?.class, props.pt?.root?.class));
|
||||
const paginatorClasses = computed(() => twMerge(classes.value.paginator?.class, props.pt?.paginator?.class));
|
||||
const activePaginator = computed(() => twMerge(classes.value.activePaginator?.class, props.pt?.activePaginator?.class));
|
||||
|
||||
if (props.currentPage && !props.modelValue) {
|
||||
throw new Error('currentPage/current-page is deprecated, use v-model instead to set the current page.');
|
||||
}
|
||||
|
||||
if (!props.modelValue) {
|
||||
throw new TypeError('v-model is required for the paginate component.');
|
||||
}
|
||||
|
||||
const currentPageRef = toRef(props, 'modelValue');
|
||||
const emit = defineEmits(['update:modelValue', 'click', 'update:paginatedItems']);
|
||||
|
||||
const onClickHandler = (number: number) => {
|
||||
if (number === currentPageRef.value || number > totalPages.value || number < 1 || props.disablePagination) return;
|
||||
emit('update:modelValue', number);
|
||||
emit('click', number);
|
||||
};
|
||||
|
||||
const NumbersLocale = (number: number) => {
|
||||
switch (props.locale) {
|
||||
case 'ar':
|
||||
return number.toLocaleString('ar-SA');
|
||||
case 'ir':
|
||||
return number.toLocaleString('fa-IR');
|
||||
default:
|
||||
return number;
|
||||
}
|
||||
};
|
||||
|
||||
const navigationHandler = (page: number) => {
|
||||
if (props.type !== 'link') return '';
|
||||
return props.linkUrl.replace('[page]', page.toString());
|
||||
};
|
||||
|
||||
const totalPages = computed(() => Math.ceil(props.items.length / props.itemsPerPage));
|
||||
|
||||
const paginatedItems = computed(() => {
|
||||
const start = (currentPageRef.value - 1) * props.itemsPerPage;
|
||||
const end = start + props.itemsPerPage;
|
||||
return props.items.slice(start, end);
|
||||
});
|
||||
|
||||
watch(currentPageRef, emitPaginatedItems);
|
||||
onMounted(emitPaginatedItems);
|
||||
|
||||
function emitPaginatedItems() {
|
||||
emit('update:paginatedItems', paginatedItems.value);
|
||||
}
|
||||
|
||||
const paginate = computed(() => {
|
||||
let startPage: number, endPage: number;
|
||||
if (totalPages.value <= props.maxPagesShown) {
|
||||
startPage = 1;
|
||||
endPage = totalPages.value;
|
||||
} else {
|
||||
const maxPagesShownBeforeCurrentPage = Math.floor(props.maxPagesShown / 2);
|
||||
const maxPagesShownAfterCurrentPage = Math.ceil(props.maxPagesShown / 2) - 1;
|
||||
if (currentPageRef.value <= maxPagesShownBeforeCurrentPage) {
|
||||
startPage = 1;
|
||||
endPage = props.maxPagesShown;
|
||||
} else if (currentPageRef.value + maxPagesShownAfterCurrentPage >= totalPages.value) {
|
||||
startPage = totalPages.value - props.maxPagesShown + 1;
|
||||
endPage = totalPages.value;
|
||||
} else {
|
||||
startPage = currentPageRef.value - maxPagesShownBeforeCurrentPage;
|
||||
endPage = currentPageRef.value + maxPagesShownAfterCurrentPage;
|
||||
}
|
||||
}
|
||||
let pages = Array.from(Array(endPage + 1 - startPage).keys()).map((i) => startPage + i);
|
||||
|
||||
if (props.dir === 'rtl') {
|
||||
pages = pages.reverse();
|
||||
}
|
||||
|
||||
return { currentPage: currentPageRef.value, itemsPerPage: props.itemsPerPage, totalPages: totalPages, startPage, endPage, pages };
|
||||
});
|
||||
|
||||
const isRtl = computed(() => props.dir === 'rtl');
|
||||
const backButtonIfCondition = computed(() =>
|
||||
isRtl.value ? !props.hidePrevNextWhenEnds || currentPageRef.value !== totalPages.value : !props.hidePrevNextWhenEnds || currentPageRef.value !== 1
|
||||
);
|
||||
const nextButtonIfCondition = computed(() =>
|
||||
isRtl.value ? !props.hidePrevNextWhenEnds || currentPageRef.value !== 1 : !props.hidePrevNextWhenEnds || currentPageRef.value !== totalPages.value
|
||||
);
|
||||
const startingBreakPointButtonIfCondition = computed(() => (isRtl.value ? paginate.value.pages[0] < totalPages.value - 1 : paginate.value.pages[0] >= 3));
|
||||
const endingBreakPointButtonIfCondition = computed(() =>
|
||||
isRtl.value ? paginate.value.pages[paginate.value.pages.length - 1] >= 3 : paginate.value.pages[paginate.value.pages.length - 1] < totalPages.value - 1
|
||||
);
|
||||
const firstButtonIfCondition = computed(() => (isRtl.value ? paginate.value.pages[0] < totalPages.value : paginate.value.pages[0] >= 2));
|
||||
const lastButtonIfCondition = computed(() =>
|
||||
isRtl.value ? paginate.value.pages[paginate.value.pages.length - 1] >= 2 : paginate.value.pages[paginate.value.pages.length - 1] < totalPages.value
|
||||
);
|
||||
const firstPageButtonIfCondition = computed(() => currentPageRef.value !== 1);
|
||||
const lastPageButtonIfCondition = computed(() => currentPageRef.value !== totalPages.value);
|
||||
|
||||
if (props.type === 'link' && (props.linkUrl === '#' || !props.linkUrl.includes('[page]'))) {
|
||||
throw new TypeError('linkUrl must contain "[page]" if type is "link".');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul id="componentContainer" :class="rootClasses">
|
||||
<!-- Go back to first page Button -->
|
||||
<li v-if="showEndingButtons && firstPageButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? totalPages : 1)"
|
||||
:class="[firstPageButtonClass, paginatorClasses, disablePagination ? disabledPaginateButtonsClass : '']"
|
||||
:disabled="disablePagination"
|
||||
@click="emitPaginatedItems()"
|
||||
@click.prevent="onClickHandler(isRtl ? totalPages : 1)"
|
||||
>
|
||||
<slot name="first-page-button">
|
||||
{{ firstPageContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Backward Jump Button -->
|
||||
<li v-if="showJumpButtons && startingBreakPointButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? currentPageRef + Math.ceil(maxPagesShown / 2) : currentPageRef - Math.ceil(maxPagesShown / 2))"
|
||||
:class="[
|
||||
backwardJumpButtonClass,
|
||||
paginateButtonsClass,
|
||||
disablePagination ? disabledPaginateButtonsClass : '',
|
||||
disablePagination ? disabledBackwardJumpButtonClass : ''
|
||||
]"
|
||||
:disabled="disablePagination"
|
||||
@click.prevent="onClickHandler(isRtl ? currentPageRef + Math.ceil(maxPagesShown / 2) : currentPageRef - Math.ceil(maxPagesShown / 2))"
|
||||
@click="emitPaginatedItems()"
|
||||
>
|
||||
<slot name="backward-jump-button">
|
||||
{{ backwardJumpButtonContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Back Button -->
|
||||
<li v-if="!hidePrevNext && backButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? currentPageRef + 1 : currentPageRef - 1)"
|
||||
:class="[
|
||||
backButtonClass,
|
||||
paginateButtonsClass,
|
||||
disablePagination ? disabledPaginateButtonsClass : '',
|
||||
disablePagination ? disabledBackButtonClass : ''
|
||||
]"
|
||||
:disabled="disablePagination"
|
||||
@click.prevent="onClickHandler(isRtl ? currentPageRef + 1 : currentPageRef - 1)"
|
||||
@click="emitPaginatedItems()"
|
||||
>
|
||||
<slot name="prev-button">
|
||||
{{ prevButtonContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- First Button before Starting Breakpoint Button -->
|
||||
<li v-if="showBreakpointButtons && firstButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? totalPages : 1)"
|
||||
:class="[
|
||||
firstButtonClass,
|
||||
paginateButtonsClass,
|
||||
disablePagination ? disabledPaginateButtonsClass : '',
|
||||
disablePagination ? disabledFirstButtonClass : ''
|
||||
]"
|
||||
:disabled="disablePagination"
|
||||
@click.prevent="onClickHandler(isRtl ? totalPages : 1)"
|
||||
@click="emitPaginatedItems()"
|
||||
>
|
||||
{{ isRtl ? NumbersLocale(totalPages) : NumbersLocale(1) }}
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Starting Breakpoint Button -->
|
||||
<li v-if="showBreakpointButtons && startingBreakPointButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="
|
||||
navigationHandler(
|
||||
disableBreakpointButtons
|
||||
? currentPageRef
|
||||
: isRtl
|
||||
? currentPageRef + Math.ceil(maxPagesShown / 2)
|
||||
: currentPageRef - Math.ceil(maxPagesShown / 2)
|
||||
)
|
||||
"
|
||||
:disabled="disableBreakpointButtons || disablePagination"
|
||||
:class="[
|
||||
startingBreakpointButtonClass,
|
||||
paginateButtonsClass,
|
||||
disableBreakpointButtons || disablePagination ? `${disabledPaginateButtonsClass} ${disabledBreakPointButtonClass}` : ''
|
||||
]"
|
||||
@click="emitPaginatedItems()"
|
||||
@click.prevent="
|
||||
onClickHandler(
|
||||
disableBreakpointButtons
|
||||
? currentPageRef
|
||||
: isRtl
|
||||
? currentPageRef + Math.ceil(maxPagesShown / 2)
|
||||
: currentPageRef - Math.ceil(maxPagesShown / 2)
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot name="starting-breakpoint-button">
|
||||
{{ startingBreakpointContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Numbers Buttons -->
|
||||
<li v-for="(page, index) in paginate.pages" :key="index">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(page)"
|
||||
:class="[
|
||||
paginateButtonsClass,
|
||||
numberButtonsClass,
|
||||
page === currentPageRef ? activePaginator : '',
|
||||
disablePagination ? disabledPaginateButtonsClass : ''
|
||||
]"
|
||||
:disabled="disablePagination"
|
||||
@click.prevent="() => onClickHandler(page)"
|
||||
@click="emitPaginatedItems()"
|
||||
>
|
||||
{{ NumbersLocale(page) }}
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Ending Breakpoint Button -->
|
||||
<li v-if="showBreakpointButtons && endingBreakPointButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="
|
||||
navigationHandler(
|
||||
disableBreakpointButtons
|
||||
? currentPageRef
|
||||
: isRtl
|
||||
? currentPageRef - Math.ceil(maxPagesShown / 2)
|
||||
: currentPageRef + Math.ceil(maxPagesShown / 2)
|
||||
)
|
||||
"
|
||||
:disabled="disableBreakpointButtons || disablePagination"
|
||||
:class="[
|
||||
endingBreakPointButtonClass,
|
||||
paginateButtonsClass,
|
||||
disableBreakpointButtons || disablePagination ? `${disabledPaginateButtonsClass} ${disabledBreakPointButtonClass}` : ''
|
||||
]"
|
||||
@click="emitPaginatedItems()"
|
||||
@click.prevent="
|
||||
onClickHandler(
|
||||
disableBreakpointButtons
|
||||
? currentPageRef
|
||||
: isRtl
|
||||
? currentPageRef - Math.ceil(maxPagesShown / 2)
|
||||
: currentPageRef + Math.ceil(maxPagesShown / 2)
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot name="ending-breakpoint-button">
|
||||
{{ endingBreakpointButtonContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Last Button after Ending Breakingpoint Button-->
|
||||
<li v-if="showBreakpointButtons && lastButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? 1 : totalPages)"
|
||||
:class="[
|
||||
lastButtonClass,
|
||||
paginateButtonsClass,
|
||||
disablePagination ? disabledPaginateButtonsClass : '',
|
||||
disablePagination ? disabledLastButtonClass : ''
|
||||
]"
|
||||
:disabled="disablePagination"
|
||||
@click="emitPaginatedItems()"
|
||||
@click.prevent="onClickHandler(isRtl ? 1 : totalPages)"
|
||||
>
|
||||
{{ isRtl ? NumbersLocale(1) : NumbersLocale(totalPages) }}
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Next Button -->
|
||||
<li v-if="!hidePrevNext && nextButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? currentPageRef - 1 : currentPageRef + 1)"
|
||||
:class="[
|
||||
paginateButtonsClass,
|
||||
nextButtonClass,
|
||||
disablePagination ? disabledPaginateButtonsClass : '',
|
||||
disablePagination ? disabledNextButtonClass : ''
|
||||
]"
|
||||
:disabled="disablePagination"
|
||||
@click.prevent="onClickHandler(isRtl ? currentPageRef - 1 : currentPageRef + 1)"
|
||||
@click="emitPaginatedItems()"
|
||||
>
|
||||
<slot name="next-button">
|
||||
{{ nextButtonContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Forward Jump Button -->
|
||||
<li v-if="showJumpButtons && endingBreakPointButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? currentPageRef - Math.ceil(maxPagesShown / 2) : currentPageRef + Math.ceil(maxPagesShown / 2))"
|
||||
:class="[
|
||||
forwardJumpButtonClass,
|
||||
paginateButtonsClass,
|
||||
disablePagination ? disabledPaginateButtonsClass : '',
|
||||
disablePagination ? disabledForwardJumpButtonClass : ''
|
||||
]"
|
||||
:disabled="disablePagination"
|
||||
@click.prevent="onClickHandler(isRtl ? currentPageRef - Math.ceil(maxPagesShown / 2) : currentPageRef + Math.ceil(maxPagesShown / 2))"
|
||||
@click="emitPaginatedItems()"
|
||||
>
|
||||
<slot name="forward-jump-button">
|
||||
{{ forwardJumpButtonContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
|
||||
<!-- Go forward to last page -->
|
||||
<li v-if="showEndingButtons && lastPageButtonIfCondition">
|
||||
<component
|
||||
:is="type === 'button' ? 'button' : 'a'"
|
||||
:href="navigationHandler(isRtl ? 1 : totalPages)"
|
||||
:class="[lastPageButtonClass, paginateButtonsClass, disablePagination ? disabledPaginateButtonsClass : '']"
|
||||
:disabled="disablePagination"
|
||||
@click.prevent="onClickHandler(isRtl ? 1 : totalPages)"
|
||||
@click="emitPaginatedItems()"
|
||||
>
|
||||
<slot name="last-page-button">
|
||||
{{ lastPageContent }}
|
||||
</slot>
|
||||
</component>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
11
components/Ml/Paginator/types.ts
Normal file
11
components/Ml/Paginator/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export type PTAttribs = {
|
||||
root?: {
|
||||
class: string;
|
||||
};
|
||||
paginator?: {
|
||||
class: string;
|
||||
};
|
||||
activePaginator?: {
|
||||
class: string;
|
||||
};
|
||||
};
|
139
components/Ml/Slider/MlSlider.vue
Normal file
139
components/Ml/Slider/MlSlider.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div v-show="showSwiper" class="relative" :class="rootClasses">
|
||||
<swiper
|
||||
loop-add-blank-slides
|
||||
:class="sliderClasses"
|
||||
:direction="direction"
|
||||
:breakpoints="{
|
||||
450: { slidesPerView: 1.2, spaceBetween: 20 },
|
||||
500: { slidesPerView: 1.5, spaceBetween: 20 },
|
||||
600: { slidesPerView: 1.5, spaceBetween: 20 },
|
||||
770: { slidesPerView: 1, spaceBetween: 20 },
|
||||
800: { slidesPerView: 1, spaceBetween: 20 },
|
||||
1000: { slidesPerView: 1.2, spaceBetween: 20 },
|
||||
1200: { slidesPerView: 1.5, spaceBetween: 20 },
|
||||
1600: { slidesPerView: 2, spaceBetween: 40 },
|
||||
2000: { slidesPerView: 2.5, spaceBetween: 40 },
|
||||
}"
|
||||
:space-between="50"
|
||||
@swiper="onSwiper"
|
||||
@slide-change="onSlideChange"
|
||||
>
|
||||
<swiper-slide
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
class="hover:cursor-pointer"
|
||||
>
|
||||
<slot :item="item" />
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
<div>
|
||||
<div :class="counterClasses">
|
||||
<div
|
||||
class="bg-BgDark dark:bg-BgLight mt-4 h-1 transition-all duration-500"
|
||||
:style="`width: ${progerssBar}%`"
|
||||
></div>
|
||||
<div class="flex justify-between my-4">
|
||||
<button
|
||||
:disabled="isPrevDisabled"
|
||||
class="px-4 py-2 disabled:text-gray-400 cursor-pointer"
|
||||
@click="
|
||||
useRipple($event, true);
|
||||
goPrev();
|
||||
"
|
||||
>
|
||||
<ArrowLeftIcon class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
:disabled="isNextDisabled"
|
||||
class="px-4 py-2 disabled:text-gray-400 cursor-pointer"
|
||||
@click="
|
||||
useRipple($event, true);
|
||||
goNext();
|
||||
"
|
||||
>
|
||||
<ArrowRightIcon class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineProps, withDefaults } from "vue";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import type { Swiper as swiper } from "swiper/types";
|
||||
import "swiper/css";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/vue/24/solid";
|
||||
import type { PTAttribs, SliderProps } from "./types";
|
||||
|
||||
const showSwiper = ref(false);
|
||||
const progerssBar = ref(0);
|
||||
const prevDisabled = ref(true);
|
||||
const nextDisabled = ref(true);
|
||||
|
||||
let swiperInstance: swiper;
|
||||
|
||||
const onSwiper = (swiper: swiper) => {
|
||||
progerssBar.value = Math.ceil(
|
||||
((swiper.realIndex + (swiper.params.slidesPerView as number)) /
|
||||
swiper.el.querySelectorAll(".swiper-slide").length) *
|
||||
100
|
||||
);
|
||||
swiperInstance = swiper;
|
||||
showSwiper.value = true;
|
||||
prevDisabled.value = !swiper.params.loop && swiper.realIndex == 0;
|
||||
nextDisabled.value = swiperInstance.isEnd;
|
||||
};
|
||||
|
||||
const onSlideChange = (swiper: swiper) => {
|
||||
progerssBar.value = Math.ceil(
|
||||
((swiper.realIndex + (swiper.params.slidesPerView as number)) /
|
||||
swiper.slides.length) *
|
||||
100
|
||||
);
|
||||
prevDisabled.value = !swiper.params.loop && swiper.realIndex == 0;
|
||||
nextDisabled.value = swiper.isEnd;
|
||||
};
|
||||
|
||||
const isPrevDisabled = computed(() => prevDisabled.value);
|
||||
const isNextDisabled = computed(() => nextDisabled.value);
|
||||
|
||||
function goNext() {
|
||||
swiperInstance.slideNext();
|
||||
}
|
||||
|
||||
function goPrev() {
|
||||
swiperInstance.slidePrev();
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SliderProps>(), {
|
||||
items: [],
|
||||
});
|
||||
|
||||
const classes = ref({
|
||||
root: {
|
||||
class: ``,
|
||||
},
|
||||
slider: {
|
||||
class: ``,
|
||||
},
|
||||
counter: {
|
||||
class: `max-w-[1200px] mx-auto px-6 container`,
|
||||
},
|
||||
} as PTAttribs);
|
||||
|
||||
const rootClasses = computed(() =>
|
||||
twMerge(classes.value.root?.class, props.pt?.root?.class)
|
||||
);
|
||||
const sliderClasses = computed(() =>
|
||||
twMerge(classes.value.slider?.class, props.pt?.slider?.class)
|
||||
);
|
||||
const counterClasses = computed(() =>
|
||||
twMerge(classes.value.counter?.class, props.pt?.counter?.class)
|
||||
);
|
||||
|
||||
const items = props.items;
|
||||
</script>
|
22
components/Ml/Slider/types.ts
Normal file
22
components/Ml/Slider/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export type SliderProps = {
|
||||
direction: 'horizontal' | 'vertical';
|
||||
pt?: PTAttribs;
|
||||
items: Array<[{ title: string; info: string }]>;
|
||||
};
|
||||
|
||||
export type PTAttribs = {
|
||||
root?: {
|
||||
class: string;
|
||||
};
|
||||
slider?: {
|
||||
class: string;
|
||||
};
|
||||
counter?: {
|
||||
class: string;
|
||||
};
|
||||
direction?: {
|
||||
direction: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type spinerPlace = 'suffix' | 'prefix';
|
111
components/Ml/SliderPoint/MlSliderPoint.vue
Normal file
111
components/Ml/SliderPoint/MlSliderPoint.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div v-show="showSwiper" class="relative" :class="rootClasses">
|
||||
<swiper
|
||||
loop-add-blank-slides
|
||||
:class="sliderClasses"
|
||||
:direction="direction"
|
||||
:space-between="50"
|
||||
:breakpoints="{
|
||||
2000: { slidesPerView: 1, spaceBetween: 40 },
|
||||
}"
|
||||
@swiper="onSwiper"
|
||||
@slide-change="onSlideChange"
|
||||
>
|
||||
<swiper-slide
|
||||
v-for="slide in 4"
|
||||
:key="slide"
|
||||
class="h-full hover:cursor-pointer"
|
||||
>
|
||||
<slot />
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
<div
|
||||
class="flex justify-between items-center h-10 cursor-pointer"
|
||||
:class="counterClasses"
|
||||
>
|
||||
<div
|
||||
v-for="(slide, index) in 4"
|
||||
:key="index"
|
||||
:class="[
|
||||
'bg-BgDark dark:bg-BgLight h-[3px] transition-all duration-500',
|
||||
index === currentSlideNumber
|
||||
? 'py-2 w-[16px] rounded-3xl'
|
||||
: 'w-[25px] rounded-sm md:w-[24px]',
|
||||
]"
|
||||
@click="goToSlide(index), stopAutoSlide()"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineProps, withDefaults, onMounted } from "vue";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import type { Swiper as swiper } from "swiper/types";
|
||||
import "swiper/css";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { PTAttribs } from "./types";
|
||||
|
||||
const showSwiper = ref(false);
|
||||
const currentSlideNumber = ref(0);
|
||||
let swiperInstance: swiper | null = null;
|
||||
let intervalId: number | null = null;
|
||||
|
||||
const onSwiper = (swiper: swiper) => {
|
||||
swiperInstance = swiper;
|
||||
showSwiper.value = true;
|
||||
};
|
||||
|
||||
const onSlideChange = (swiper: swiper) => {
|
||||
currentSlideNumber.value = swiper.realIndex;
|
||||
};
|
||||
|
||||
function startAutoSlide() {
|
||||
intervalId = setInterval(() => {
|
||||
const nextSlide = (currentSlideNumber.value + 1) % 4;
|
||||
goToSlide(nextSlide);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function goToSlide(index: number) {
|
||||
swiperInstance?.slideTo(index);
|
||||
currentSlideNumber.value = index;
|
||||
}
|
||||
|
||||
function stopAutoSlide() {
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startAutoSlide();
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
direction: "horizontal" | "vertical";
|
||||
pt?: PTAttribs;
|
||||
}>(),
|
||||
{
|
||||
direction: "horizontal",
|
||||
}
|
||||
);
|
||||
|
||||
const classes = ref({
|
||||
root: { class: `` },
|
||||
slider: { class: `` },
|
||||
counter: { class: `mx-auto px-6 container` },
|
||||
} as PTAttribs);
|
||||
|
||||
const rootClasses = computed(() =>
|
||||
twMerge(classes.value.root?.class, props.pt?.root?.class)
|
||||
);
|
||||
const sliderClasses = computed(() =>
|
||||
twMerge(classes.value.slider?.class, props.pt?.slider?.class)
|
||||
);
|
||||
const counterClasses = computed(() =>
|
||||
twMerge(classes.value.counter?.class, props.pt?.counter?.class)
|
||||
);
|
||||
</script>
|
21
components/Ml/SliderPoint/types.ts
Normal file
21
components/Ml/SliderPoint/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export type SliderProps = {
|
||||
rounded?: boolean;
|
||||
pt?: PTAttribs;
|
||||
};
|
||||
|
||||
export type PTAttribs = {
|
||||
root?: {
|
||||
class: string;
|
||||
};
|
||||
slider?: {
|
||||
class: string;
|
||||
};
|
||||
counter?: {
|
||||
class: string;
|
||||
};
|
||||
direction?: {
|
||||
direction: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type spinerPlace = 'suffix' | 'prefix';
|
30
components/Ml/Spinner/MlSpinner.vue
Normal file
30
components/Ml/Spinner/MlSpinner.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<svg :class="spinnerClasses" fill="none" role="status" viewBox="0 0 100 101" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs } from 'vue';
|
||||
import { useSpinnerClasses } from './composables/useSpinnerClasses';
|
||||
import type { SpinnerColor, SpinnerSize } from './types';
|
||||
|
||||
interface ISpinnerProps {
|
||||
color?: SpinnerColor;
|
||||
size?: SpinnerSize;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ISpinnerProps>(), {
|
||||
color: 'blue',
|
||||
size: '4'
|
||||
});
|
||||
|
||||
const { spinnerClasses } = useSpinnerClasses(toRefs(props));
|
||||
</script>
|
52
components/Ml/Spinner/composables/useSpinnerClasses.ts
Normal file
52
components/Ml/Spinner/composables/useSpinnerClasses.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { computed, type Ref } from 'vue';
|
||||
import classNames from 'classnames';
|
||||
import type { SpinnerColor, SpinnerSize } from '../types';
|
||||
|
||||
const sizes: Record<SpinnerSize, string> = {
|
||||
0: 'w-0 h-0',
|
||||
0.5: 'w-0.5 h-0.5',
|
||||
1: 'w-1 h-1',
|
||||
1.5: 'w-1.5 h-1.5',
|
||||
10: 'w-10 h-10',
|
||||
11: 'w-11 h-11',
|
||||
12: 'w-12 h-12',
|
||||
2: 'w-2 h-2',
|
||||
2.5: 'w-2.5 h-2.5',
|
||||
3: 'w-3 h-3',
|
||||
4: 'w-4 h-4',
|
||||
5: 'w-5 h-5',
|
||||
6: 'w-6 h-6',
|
||||
7: 'w-7 h-7',
|
||||
8: 'w-8 h-8',
|
||||
9: 'w-9 h-9'
|
||||
};
|
||||
|
||||
const colors: Record<SpinnerColor, string> = {
|
||||
blue: 'fill-blue-600',
|
||||
gray: 'fill-gray-600 dark:fill-gray-300',
|
||||
green: 'fill-green-500',
|
||||
pink: 'fill-pink-600',
|
||||
purple: 'fill-purple-600',
|
||||
red: 'fill-red-600',
|
||||
white: 'fill-white',
|
||||
yellow: 'fill-yellow-400',
|
||||
accent1: 'fill-accent1',
|
||||
accent2: 'fill-accent2'
|
||||
};
|
||||
|
||||
export type UseSpinnerClassesProps = {
|
||||
color: Ref<SpinnerColor>;
|
||||
size: Ref<SpinnerSize>;
|
||||
};
|
||||
|
||||
export function useSpinnerClasses(props: UseSpinnerClassesProps): {
|
||||
spinnerClasses: Ref<string>;
|
||||
} {
|
||||
const sizeClasses = computed(() => sizes[props.size.value]);
|
||||
const colorClasses = computed(() => colors[props.color.value]);
|
||||
const bgColorClasses = computed(() => 'text-gray-200 dark:text-gray-400');
|
||||
const animateClasses = computed(() => 'animate-spin');
|
||||
const spinnerClasses = computed(() => classNames(animateClasses.value, bgColorClasses.value, colorClasses.value, sizeClasses.value));
|
||||
|
||||
return { spinnerClasses };
|
||||
}
|
3
components/Ml/Spinner/types.ts
Normal file
3
components/Ml/Spinner/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type SpinnerColor = 'blue' | 'gray' | 'green' | 'red' | 'yellow' | 'pink' | 'purple' | 'white' | 'accent1' | 'accent2';
|
||||
|
||||
export type SpinnerSize = '0' | '0.5' | '1' | '1.5' | '2' | '2.5' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12';
|
46
components/Ml/Tabber/MlTabberBase.vue
Normal file
46
components/Ml/Tabber/MlTabberBase.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="flex md:flex-row flex-col">
|
||||
<div class="flex flex-col justify-between md:w-[40%] cursor-pointer">
|
||||
<div
|
||||
v-for="(el, index) in props.tabs"
|
||||
:key="index"
|
||||
:class="[
|
||||
'w-full px-6 py-4 md:py-8 rtl:text-right text-left border-b border-gray-400 md:text-[18px] md:last:border-b-0 md:h-full flex items-center',
|
||||
index === indexItem
|
||||
? 'md:border-r-0 font-[700]'
|
||||
: 'md:border-r font-medium',
|
||||
]"
|
||||
@click="
|
||||
showContent(index);
|
||||
useRipple($event, true);
|
||||
"
|
||||
>
|
||||
{{ el.header }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="tabs[indexItem]" class="md:flex-1 h-auto">
|
||||
<div class="flex flex-col items-start p-6 md:p-16">
|
||||
<!-- Your image -->
|
||||
<h1 class="my-5 font-bold text-2xl md:text-[32px]">
|
||||
{{ tabs[indexItem].content.title }}
|
||||
</h1>
|
||||
<p class="font-ibm text-[16px] md:text-[18px]">
|
||||
{{ tabs[indexItem].content.info }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const indexItem = ref(0);
|
||||
|
||||
function showContent(index) {
|
||||
indexItem.value = index;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
tabs: Object,
|
||||
});
|
||||
</script>
|
34
components/Ml/ThemeSwitch/MlDark.vue
Normal file
34
components/Ml/ThemeSwitch/MlDark.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.25 8.75C15.5 8.75 15 8.25 15 7.5V6.25C15 5.5 15.5 5 16.25 5C17 5 17.5 5.5 17.5 6.25V7.5C17.5 8.25 17 8.75 16.25 8.75Z" fill="white" />
|
||||
<path
|
||||
d="M7.375 12.375C7 12.375 6.75 12.25 6.5 12L3.875 9.375C3.375 8.875 3.375 8.125 3.875 7.625C4.375 7.125 5.125 7.125 5.625 7.625L8.25 10.25C8.75 10.75 8.75 11.5 8.25 12C8 12.25 7.75 12.375 7.375 12.375Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M3.75 21.25H2.5C1.75 21.25 1.25 20.75 1.25 20C1.25 19.25 1.75 18.75 2.5 18.75H3.75C4.5 18.75 5 19.25 5 20C5 20.75 4.5 21.25 3.75 21.25Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M4.75 32.75C4.375 32.75 4.125 32.625 3.875 32.375C3.375 31.875 3.375 31.125 3.875 30.625L6.5 28C7 27.5 7.75 27.5 8.25 28C8.75 28.5 8.75 29.25 8.25 29.75L5.625 32.375C5.375 32.625 5.125 32.75 4.75 32.75Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M16.25 35C15.5 35 15 34.5 15 33.75V32.5C15 31.75 15.5 31.25 16.25 31.25C17 31.25 17.5 31.75 17.5 32.5V33.75C17.5 34.5 17 35 16.25 35Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 31.25C21.25 31.25 16.25 26.25 16.25 20C16.25 13.75 21.25 8.75 27.5 8.75C33.75 8.75 38.75 13.75 38.75 20C38.75 26.25 33.75 31.25 27.5 31.25Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M13.75 20C13.75 16.125 15.375 12.625 17.875 10.125C17.375 10 16.875 10 16.25 10C10.75 10 6.25 14.5 6.25 20C6.25 25.5 10.75 30 16.25 30C16.875 30 17.375 30 17.875 29.875C15.375 27.375 13.75 23.875 13.75 20Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M12.4083 25.2228L12.4083 25.2228L12.4123 25.2249C13.409 25.758 14.5207 26.02 15.7361 26.02C16.9515 26.02 18.0632 25.758 19.0599 25.2249L19.0599 25.2249L19.0639 25.2228C20.0574 24.6798 20.8437 23.9298 21.4133 22.9765C21.9859 22.0182 22.2681 20.9417 22.2681 19.76C22.2681 18.5783 21.9859 17.5018 21.4133 16.5435C20.8435 15.5899 20.0565 14.8446 19.0619 14.3122C18.0649 13.7678 16.9525 13.5 15.7361 13.5C14.5197 13.5 13.4073 13.7678 12.4103 14.3122C11.4145 14.8452 10.6276 15.5966 10.0582 16.5607C9.48652 17.5179 9.2041 18.5885 9.2041 19.76C9.2041 20.932 9.48673 22.0073 10.0575 22.9742L10.0575 22.9742L10.0589 22.9765C10.6285 23.9298 11.4148 24.6798 12.4083 25.2228ZM18.7241 17.9888L18.7241 17.9888L18.726 17.9921C19.0205 18.5075 19.1721 19.0927 19.1721 19.76C19.1721 20.4265 19.0208 21.0181 18.725 21.5456C18.4286 22.0637 18.0248 22.4676 17.507 22.7642C16.9913 23.049 16.4049 23.196 15.7361 23.196C15.0673 23.196 14.4809 23.049 13.9652 22.7642C13.4474 22.4676 13.0436 22.0636 12.7472 21.5456C12.4514 21.018 12.3001 20.4265 12.3001 19.76C12.3001 19.0927 12.4517 18.5075 12.7462 17.9921L12.7462 17.9921L12.7481 17.9888C13.045 17.4599 13.448 17.0581 13.9623 16.7734L13.9623 16.7735L13.9682 16.7701C14.4836 16.4756 15.0688 16.324 15.7361 16.324C16.4034 16.324 16.9886 16.4756 17.504 16.7701L17.504 16.7702L17.5099 16.7734C18.0242 17.0581 18.4272 17.4599 18.7241 17.9888ZM24.5214 25.63L24.5232 25.6306C25.3817 25.8904 26.2679 26.02 27.18 26.02C28.2095 26.02 29.1139 25.8637 29.8764 25.5301C30.6249 25.2026 31.216 24.7483 31.6113 24.1504C32.009 23.5651 32.208 22.9077 32.208 22.192C32.208 21.3782 31.9911 20.6727 31.5061 20.1322C31.0798 19.6459 30.5603 19.2854 29.9538 19.0542C29.3896 18.8289 28.6826 18.6202 27.84 18.4256C27.2391 18.278 26.775 18.1486 26.4421 18.0377C26.1428 17.9379 25.9115 17.8084 25.7355 17.6576C25.6257 17.5566 25.568 17.4339 25.568 17.248C25.568 16.9628 25.6772 16.7439 25.9449 16.5549C26.2284 16.3622 26.7208 16.228 27.5 16.228C27.9513 16.228 28.4232 16.2952 28.9172 16.4335C29.408 16.5709 29.8803 16.7724 30.3349 17.0392L30.8337 17.332L31.0513 16.7961L31.7073 15.1801L31.8607 14.8022L31.5214 14.576C30.987 14.2198 30.3628 13.954 29.6569 13.7719C28.9548 13.5907 28.2408 13.5 27.516 13.5C26.4865 13.5 25.5821 13.6563 24.8196 13.9899L24.8196 13.9899L24.8169 13.9911C24.0754 14.3206 23.4873 14.7816 23.084 15.3866L23.0826 15.3887C22.6964 15.9743 22.504 16.6306 22.504 17.344C22.504 18.1649 22.7135 18.8778 23.1837 19.4286L23.1836 19.4287L23.1924 19.4385C23.6275 19.922 24.1457 20.2825 24.7432 20.5142L24.7432 20.5142L24.7464 20.5154C25.3073 20.7285 26.0229 20.9368 26.8865 21.142C27.4881 21.2898 27.9527 21.4193 28.2859 21.5303C28.5819 21.629 28.802 21.7605 28.9632 21.9141L28.972 21.9225L28.9812 21.9304C29.1019 22.0347 29.16 22.156 29.16 22.336C29.16 22.5784 29.0607 22.7823 28.7662 22.9711L28.7661 22.971L28.7612 22.9743C28.4771 23.1612 27.9762 23.292 27.18 23.292C26.5661 23.292 25.9489 23.1901 25.3267 22.9832C24.709 22.7635 24.2012 22.4897 23.7941 22.1678L23.2892 21.7686L23.0273 22.3565L22.3073 23.9725L22.1562 24.3117L22.4372 24.5544C22.974 25.018 23.677 25.3711 24.5214 25.63Z"
|
||||
fill="black"
|
||||
stroke="white"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
38
components/Ml/ThemeSwitch/MlLight.vue
Normal file
38
components/Ml/ThemeSwitch/MlLight.vue
Normal file
File diff suppressed because one or more lines are too long
31
components/Ml/ThemeSwitch/MlThemeSwitch.vue
Normal file
31
components/Ml/ThemeSwitch/MlThemeSwitch.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<span class="flex min-w-8 cursor-pointer" @click="chanegMode">
|
||||
<ClientOnly>
|
||||
<span :title="formatedModeName">
|
||||
<DarkComponent v-if="colormode.value == 'dark'" class="w-8" />
|
||||
<LightComponent v-if="colormode.value == 'light'" class="w-8" />
|
||||
</span>
|
||||
</ClientOnly>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LightComponent from './MlLight.vue';
|
||||
import DarkComponent from './MlDark.vue';
|
||||
const colormode = useColorMode();
|
||||
function chanegMode(): void {
|
||||
if (colormode.preference == 'dark') {
|
||||
colormode.preference = 'light';
|
||||
} else {
|
||||
colormode.preference = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
const formatedModeName = computed(() => {
|
||||
if (colormode.value.length > 0) {
|
||||
return colormode.value.charAt(0).toUpperCase() + colormode.value.slice(1);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
</script>
|
7
components/Ml/ThemeSwitch/types.ts
Normal file
7
components/Ml/ThemeSwitch/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { DefineComponent } from 'vue';
|
||||
|
||||
export type DarkMode = {
|
||||
name: 'Light' | 'Dark';
|
||||
value: 'light' | 'dark';
|
||||
icon: DefineComponent;
|
||||
};
|
48
components/Ml/transition/MlTransitionFade.vue
Normal file
48
components/Ml/transition/MlTransitionFade.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { h, Transition } from 'vue';
|
||||
|
||||
export default defineNuxtComponent({
|
||||
// name: 'MaalTransitionFade',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: 'fade'
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
return h(
|
||||
Transition,
|
||||
{
|
||||
name: props.name,
|
||||
css: false,
|
||||
onBeforeEnter(el: any) {
|
||||
el.style.transition = `opacity ${props.duration}ms ${props.delay}ms ease;`;
|
||||
el.style.opacity = 0;
|
||||
},
|
||||
onEnter(el: any, done) {
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = 1;
|
||||
done();
|
||||
});
|
||||
},
|
||||
onLeave(el: any, done) {
|
||||
el.style.transition = `opacity ${props.duration}ms ${props.delay}ms ease;`;
|
||||
el.style.opacity = 0;
|
||||
setTimeout(done, props.duration);
|
||||
}
|
||||
},
|
||||
slots.default
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
10
composables/useBounce.ts
Normal file
10
composables/useBounce.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const useBounce = (): Function => {
|
||||
let bounce: NodeJS.Timeout;
|
||||
|
||||
return function (callback: Function, timeout: number = 400) {
|
||||
clearTimeout(bounce);
|
||||
bounce = setTimeout(() => {
|
||||
callback();
|
||||
}, timeout);
|
||||
};
|
||||
};
|
3
composables/useMergeClasses.ts
Normal file
3
composables/useMergeClasses.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const useMergeClasses = (componentClasses: string | string[]): string => twMerge(...componentClasses);
|
20
composables/useRipple.ts
Normal file
20
composables/useRipple.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export const useRipple = (e: Event, isRipple: boolean): void => {
|
||||
if (isRipple) {
|
||||
const initElelemnt = e.currentTarget as HTMLElement;
|
||||
initElelemnt.style.position = 'relative';
|
||||
const rippleElement = document.createElement('span');
|
||||
rippleElement.style.backgroundColor = 'rgba(255, 255, 255, 0.3)';
|
||||
rippleElement.style.width = '100%';
|
||||
rippleElement.style.height = '100%';
|
||||
rippleElement.style.position = 'absolute';
|
||||
rippleElement.style.left = '0';
|
||||
rippleElement.style.top = '0';
|
||||
rippleElement.style.transform = 'scaleX(0)';
|
||||
|
||||
initElelemnt.appendChild(rippleElement);
|
||||
|
||||
rippleElement.animate([{ transform: 'scaleX(1)', opacity: 0 }], { duration: 500 }).addEventListener('finish', () => {
|
||||
initElelemnt.removeChild(rippleElement);
|
||||
});
|
||||
}
|
||||
};
|
28
composables/useTailwindConf.ts
Normal file
28
composables/useTailwindConf.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import resolveConfig from 'tailwindcss/resolveConfig';
|
||||
import tailwindConfig from '../tailwind.config';
|
||||
import type { DefaultTheme } from 'tailwindcss/types/generated/default-theme';
|
||||
import type { DefaultColors } from 'tailwindcss/types/generated/colors';
|
||||
import type { Config, ResolvableTo, ThemeConfig } from 'tailwindcss/types/config';
|
||||
export const useTailwindConf = (): ResolvedConfig<Config> => resolveConfig(tailwindConfig);
|
||||
|
||||
export type ResolvedConfig<T extends Config> = Omit<T, 'theme'> & {
|
||||
theme: MergeThemes<UnwrapResolvables<Omit<T['theme'], 'extend'>>, T['theme'] extends { extend: infer TExtend } ? UnwrapResolvables<TExtend> : {}>;
|
||||
};
|
||||
|
||||
type MergeThemes<Overrides extends object, Extensions extends object> = {
|
||||
[K in keyof ThemeConfigResolved | keyof Overrides]: (K extends keyof Overrides
|
||||
? Overrides[K]
|
||||
: K extends keyof DefaultThemeFull
|
||||
? DefaultThemeFull[K]
|
||||
: K extends keyof ThemeConfigResolved
|
||||
? ThemeConfigResolved[K]
|
||||
: never) &
|
||||
(K extends keyof Extensions ? Extensions[K] : {});
|
||||
};
|
||||
|
||||
type UnwrapResolvables<T> = {
|
||||
[K in keyof T]: T[K] extends ResolvableTo<infer R> ? R : T[K];
|
||||
};
|
||||
|
||||
type ThemeConfigResolved = UnwrapResolvables<ThemeConfig>;
|
||||
type DefaultThemeFull = DefaultTheme & { colors: DefaultColors };
|
7
nuxt.config.ts
Normal file
7
nuxt.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
modules: ["@nuxtjs/tailwindcss", "@nuxtjs/color-mode"],
|
||||
compatibilityDate: "2024-04-03",
|
||||
devtools: { enabled: true },
|
||||
colorMode: {},
|
||||
});
|
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.1.4",
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"nuxt": "^3.12.3",
|
||||
"swiper": "^11.1.4",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"typescript": "^5.5.3",
|
||||
"vue": "latest"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.1+sha512.14e915759c11f77eac07faba4d019c193ec8637229e62ec99eefb7cf3c3b75c64447882b7c485142451ee3a6b408059cdfb7b7fa0341b975f12d0f7629c71195",
|
||||
"devDependencies": {
|
||||
"@nuxtjs/color-mode": "^3.4.2",
|
||||
"tailwindcss": "^3.4.4"
|
||||
}
|
||||
}
|
7819
pnpm-lock.yaml
generated
Normal file
7819
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
server/tsconfig.json
Normal file
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
51
tailwind.config.js
Normal file
51
tailwind.config.js
Normal file
@ -0,0 +1,51 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
gradient:
|
||||
"linear-gradient(50deg, rgba(75,250,145,1) 0%, rgba(71,122,255,1) 100%);",
|
||||
MainLightGradient:
|
||||
"linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(114,151,246,0.6502976190476191) 36%, rgba(114,151,246,0.6502976190476191) 72%, rgba(0,0,0,0) 100%)",
|
||||
MainDarkGradient:
|
||||
"linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(13,54,161,0.6502976190476191) 49%, #161616 100%);",
|
||||
FooterLightGradient:
|
||||
"linear-gradient(180deg, #F8F8F8 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0) 100%), linear-gradient(65deg, rgba(75,250,145,0.5) 0%, rgba(20,76,220,0.5) 100%)",
|
||||
FooterDarkGradient:
|
||||
"linear-gradient(180deg, #161616 0%, rgba(0,0,0,0.5) 50%, rgba(0,0,0,0) 100%), linear-gradient(65deg, rgba(20,76,220,0.5) 0%, rgba(75,250,145,0.5) 100%)",
|
||||
FormLightGradient:
|
||||
"linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(114,151,246,1) 72%)",
|
||||
FormDarkGradient:
|
||||
"linear-gradient(180deg, #161616 20%, rgba(13,54,161,1) 90%)",
|
||||
StudyLightGradient:
|
||||
"linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(72,72,72,0.10127801120448177) 14%, rgba(249,249,249,0.3253676470588235) 50%, rgba(248,248,248,1) 100%), linear-gradient(65deg, rgba(75,250,145,0.5) 0%, rgba(20,76,220,0.5) 100%)",
|
||||
StudyDarkGradient:
|
||||
"linear-gradient(180deg, rgba(22,22,22,0) 0%, rgba(22,22,22,0.3309698879551821) 50%, #161616 100%), linear-gradient(72deg, rgba(20,76,220,0.5018382352941176) 0%, rgba(75,250,145,0.4990371148459384) 100%)",
|
||||
ContactDarkGradient:
|
||||
"linear-gradient(180deg, rgba(0,0,0,0) 20%, rgba(13,54,161,1) 90%)",
|
||||
ContactLightGradient:
|
||||
"linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(114,151,246,0.7231267507002801) 72%)",
|
||||
},
|
||||
colors: {
|
||||
gradient:
|
||||
"linear-gradient(50deg, rgba(75,250,145,1) 0%, rgba(71,122,255,1) 100%);",
|
||||
accent1: "#144CDC",
|
||||
accent2: "#4BFA91",
|
||||
|
||||
BgDark: "#090909",
|
||||
BgLight: "#F8F8F8",
|
||||
|
||||
BgDarkSecond: "#161616",
|
||||
},
|
||||
screens: {
|
||||
"3xl": "2560px",
|
||||
},
|
||||
boxShadow: {
|
||||
"custom-lrb":
|
||||
"0 10px 15px -3px rgba(0, 0, 0, 0.1), -10px 0 15px -3px rgba(0, 0, 0, 0.1), 10px 0 15px -3px rgba(0, 0, 0, 0.1)",
|
||||
"custom-drb": "-3px 28px 42px 3px rgba(23, 23, 24, 1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
Loading…
Reference in New Issue
Block a user