Compare commits

...

15 Commits

Author SHA1 Message Date
burkkyy f7e474a011 make db ssl optional
Build and Push Docker Image / build (push) Successful in 2m24s
2026-06-27 03:20:16 -07:00
burkkyy 6d211a1e83 Associate package with this repo
Build and Push Docker Image / build (push) Successful in 38s
2026-06-27 02:43:03 -07:00
burkkyy 7b7330d9b4 Changes to support build
Build and Push Docker Image / build (push) Successful in 1m16s
2026-06-27 02:13:48 -07:00
burkkyy 19352c7fa6 Use vars.REGISTRY_URL
Build and Push Docker Image / build (push) Failing after 10s
2026-06-27 01:47:50 -07:00
burkkyy ec6b9d2403 swapping back to github.server_url as ssl was setup
Build and Push Docker Image / build (push) Failing after 5s
2026-06-27 01:36:18 -07:00
burkkyy e1664bf724 Using hardcoded url for now
Build and Push Docker Image / build (push) Failing after 5s
2026-06-26 19:38:24 -07:00
burkkyy d581c7b789 Fixes
Build and Push Docker Image / build (push) Failing after 6s
2026-06-26 19:35:01 -07:00
burkkyy 8457a97c5f Build and publish
Build and Push Docker Image / build (push) Failing after 2m7s
2026-06-26 19:20:00 -07:00
burkkyy 4b91ba0936 UI upgrade 2026-06-26 02:12:22 -07:00
burkkyy 8030e98d90 Viewability upgrade 2026-06-26 01:51:24 -07:00
burkkyy 4c9e503e4f Adding how long it took to do cards 2026-06-26 01:46:18 -07:00
burkkyy cb60ec8480 flashcards in the frontend!!! 2026-06-26 01:25:25 -07:00
burkkyy f83d4d579c scss formatting 2026-06-26 01:25:07 -07:00
burkkyy 033c962ebe draggable that works 2026-06-26 01:23:26 -07:00
burkkyy 69d4f7c3f0 Visual changes 2026-06-25 19:25:13 -07:00
35 changed files with 2338 additions and 981 deletions
+16
View File
@@ -0,0 +1,16 @@
HOST_PORT=8080
API_PORT=8080
DB_HOST=db
DB_USERNAME=calebburkedev
DB_DATABASE=calebburkedev_production
DB_PASSWORD=DevPwd99!
DB_PORT=5432
DB_TRUST_SERVER_CERTIFICATE=true
VITE_APPLICATION_NAME="CALEB BURKE DEV"
VITE_API_BASE_URL="http://localhost:8080"
VITE_AUTH0_CLIENT_ID="TRlKzdNBynpo9tU1RSmnF0p8d3IEam4J"
VITE_AUTH0_AUDIENCE="alphane-api"
VITE_AUTH0_DOMAIN="https=//dev-7mdjzcgwirhocfwm.ca.auth0.com"
+34
View File
@@ -0,0 +1,34 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ vars.REGISTRY_URL }}/${{ github.repository }}:latest
${{ vars.REGISTRY_URL }}/${{ github.repository }}:${{ github.sha }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
build-args: |
RELEASE_TAG=${{ github.ref_name }}
GIT_COMMIT_HASH=${{ github.sha }}
+55
View File
@@ -1,5 +1,60 @@
# calebburke.dev # calebburke.dev
## Deploying
### Production Environment (remote)
TODO
### Test Production Build Locally
Files:
- [Dockerfile](./Dockerfile)
- [docker-compose.yml](./docker-compose.yml)
- Non-commited `.env` file
1. Create a `.env` file in top level directory with the appropriate values.
```bash
HOST_PORT=8080
API_PORT=8080
DB_HOST=db
DB_USERNAME=calebburkedev
DB_DATABASE=calebburkedev_production
DB_PASSWORD=DevPwd99!
DB_PORT=5432
DB_TRUST_SERVER_CERTIFICATE=true
VITE_APPLICATION_NAME="CALEB BURKE DEV"
VITE_API_BASE_URL="http://localhost:8080"
VITE_AUTH0_CLIENT_ID="TRlKzdNBynpo9tU1RSmnF0p8d3IEam4J"
VITE_AUTH0_AUDIENCE="alphane-api"
VITE_AUTH0_DOMAIN="https=//dev-7mdjzcgwirhocfwm.ca.auth0.com"
```
2. (optional) If testing build arguments do
```bash
docker compose build \
--build-arg RELEASE_TAG=$(date +%Y.%m.%d) \
--build-arg GIT_COMMIT_HASH=$(git rev-parse HEAD)
```
and then in the next step drop the `--build` flag.
3. Build and boot the production image via
```bash
docker compose up --build
```
4. Go to <http://localhost:3000/> and log in.
5. Navigate around the app and do some stuff and see if it works.
## Resources ## Resources
- <https://github.com/cotes2020/jekyll-theme-chirpy> - <https://github.com/cotes2020/jekyll-theme-chirpy>
+1
View File
@@ -39,6 +39,7 @@ export const DB_PASSWORD = process.env.DB_PASSWORD || ""
export const DB_DATABASE = process.env.DB_DATABASE || "" export const DB_DATABASE = process.env.DB_DATABASE || ""
export const DB_PORT = parseInt(process.env.DB_PORT || "1433") export const DB_PORT = parseInt(process.env.DB_PORT || "1433")
export const DB_TRUST_SERVER_CERTIFICATE = process.env.DB_TRUST_SERVER_CERTIFICATE === "true" export const DB_TRUST_SERVER_CERTIFICATE = process.env.DB_TRUST_SERVER_CERTIFICATE === "true"
export const DB_SSL = process.env.DB_SSL === "true"
export const REDIS_CONNECTION_URL = process.env.REDIS_CONNECTION_URL || "" export const REDIS_CONNECTION_URL = process.env.REDIS_CONNECTION_URL || ""
+2 -2
View File
@@ -7,8 +7,8 @@ import {
DB_HOST, DB_HOST,
DB_PASSWORD, DB_PASSWORD,
DB_PORT, DB_PORT,
DB_SSL,
DB_USERNAME, DB_USERNAME,
NODE_ENV,
SEQUELIZE_LOGGING, SEQUELIZE_LOGGING,
} from "@/config" } from "@/config"
import compactSql from "@/utils/compact-sql" import compactSql from "@/utils/compact-sql"
@@ -31,7 +31,7 @@ export const SEQUELIZE_CONFIG: Options<PostgresDialect> = {
password: DB_PASSWORD, password: DB_PASSWORD,
host: DB_HOST, host: DB_HOST,
port: DB_PORT, port: DB_PORT,
ssl: NODE_ENV !== "production" ? false : { rejectUnauthorized: false }, ssl: DB_SSL ? { rejectUnauthorized: false } : false,
schema: "public", // default - explicit for clarity schema: "public", // default - explicit for clarity
logging: SEQUELIZE_LOGGING ? sqlLogger : false, logging: SEQUELIZE_LOGGING ? sqlLogger : false,
pool: { pool: {
+2 -12
View File
@@ -3,7 +3,7 @@ import path from "path"
import knex, { Knex } from "knex" import knex, { Knex } from "knex"
import { isEmpty, isNil, merge } from "lodash" import { isEmpty, isNil, merge } from "lodash"
import { DB_DATABASE, DB_HOST, DB_PASSWORD, DB_PORT, DB_USERNAME, NODE_ENV } from "@/config" import { DB_DATABASE, DB_HOST, DB_PASSWORD, DB_PORT, DB_SSL, DB_USERNAME, NODE_ENV } from "@/config"
if (isEmpty(DB_DATABASE)) throw new Error("database name is unset.") if (isEmpty(DB_DATABASE)) throw new Error("database name is unset.")
if (isEmpty(DB_USERNAME)) throw new Error("database username is unset.") if (isEmpty(DB_USERNAME)) throw new Error("database username is unset.")
@@ -21,17 +21,7 @@ export function buildKnexConfig(options?: Knex.Config): Knex.Config {
password: DB_PASSWORD, password: DB_PASSWORD,
database: DB_DATABASE, database: DB_DATABASE,
port: DB_PORT, port: DB_PORT,
ssl: ssl: DB_SSL ? { rejectUnauthorized: false } : false,
NODE_ENV !== "production"
? false
: {
require: true, // Enforce SSL
rejectUnauthorized: false, // Disable certificate verification (common for Azure)
},
/* options: {
encrypt: true,
trustServerCertificate: DB_TRUST_SERVER_CERTIFICATE,
}, */
}, },
migrations: { migrations: {
directory: path.resolve(__dirname, "./migrations"), directory: path.resolve(__dirname, "./migrations"),
+4
View File
@@ -58,6 +58,10 @@ export class UsersPolicy extends PolicyFactory(User) {
"displayName", "displayName",
] ]
if (this.user.isSystemAdmin) {
attributes.push("roles")
}
return attributes return attributes
} }
View File
+4 -4
View File
@@ -20,13 +20,13 @@ services:
- .env - .env
environment: environment:
TZ: "UTC" TZ: "UTC"
POSTGRES_USER: "${DB_USER}" POSTGRES_USER: "${DB_USERNAME}"
POSTGRES_PASSWORD: "${DB_PASS}" POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: "${DB_NAME}" POSTGRES_DB: "${DB_DATABASE}"
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- pg-data:/var/lib/postgresql/data - db_data:/var/lib/postgresql/data
volumes: volumes:
db_data: db_data:
+17 -16
View File
@@ -26,10 +26,10 @@
"qs": "^6.14.0", "qs": "^6.14.0",
"validator": "^13.15.26", "validator": "^13.15.26",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-draggable-next": "^2.3.0",
"vue-i18n": "^11.3.0", "vue-i18n": "^11.3.0",
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"vue-scrollto": "^2.20.0", "vue-scrollto": "^2.20.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.11.4" "vuetify": "^3.11.4"
}, },
"devDependencies": { "devDependencies": {
@@ -5171,11 +5171,10 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/sortablejs": { "node_modules/sortablejs": {
"version": "1.15.7", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
@@ -5739,16 +5738,6 @@
} }
} }
}, },
"node_modules/vue-draggable-next": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/vue-draggable-next/-/vue-draggable-next-2.3.0.tgz",
"integrity": "sha512-ymbY0UIwfSdg0iDN/iyNNwUrTqZ/6KbPryzsvTNXBLuDCuOBdNijSK8yynNtmiSj6RapTPQfjLGQdJrZkzBd2w==",
"license": "MIT",
"peerDependencies": {
"sortablejs": "^1.14.0",
"vue": "^3.5.17"
}
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "9.4.3", "version": "9.4.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
@@ -5836,6 +5825,18 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"license": "MIT",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/vuetify": { "node_modules/vuetify": {
"version": "3.12.8", "version": "3.12.8",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.12.8.tgz", "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.12.8.tgz",
+1 -1
View File
@@ -30,10 +30,10 @@
"qs": "^6.14.0", "qs": "^6.14.0",
"validator": "^13.15.26", "validator": "^13.15.26",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-draggable-next": "^2.3.0",
"vue-i18n": "^11.3.0", "vue-i18n": "^11.3.0",
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"vue-scrollto": "^2.20.0", "vue-scrollto": "^2.20.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.11.4" "vuetify": "^3.11.4"
}, },
"devDependencies": { "devDependencies": {
-1
View File
@@ -13,7 +13,6 @@
<div <div
v-else v-else
class="d-flex" class="d-flex"
style="width: 200px"
> >
<h1 <h1
class="mt-1 ml-3" class="mt-1 ml-3"
@@ -0,0 +1,31 @@
<template>
<v-pagination
v-model="page"
:length="totalPages"
/>
</template>
<script lang="ts">
/**
* Use in conjuction with `useRouteQueryPagination` composable.
*/
</script>
<script setup lang="ts">
import { computed } from "vue"
const page = defineModel<number>({
required: true,
default: 1,
})
const perPage = defineModel<number>("perPage", {
default: 10,
})
const props = defineProps<{
totalCount: number
}>()
const totalPages = computed(() => Math.ceil(props.totalCount / perPage.value))
</script>
@@ -0,0 +1,92 @@
<template>
<v-dialog
v-model="showDialog"
width="400"
>
<v-form
ref="formRef"
@submit.prevent="validateAndCreate"
>
<v-card>
<v-card-title>Create New Flashcard Deck</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12">
<v-text-field
v-model="flashcardDeck.name"
label="Name"
:rules="[required]"
/>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn
type="submit"
color="primary"
variant="flat"
text="Create"
/>
</v-card-actions>
</v-card>
</v-form>
</v-dialog>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { useRouteQuery } from "@vueuse/router"
import { VForm } from "vuetify/components"
import { required } from "@/utils/validators"
import { booleanTransformer } from "@/utils/use-route-query-transformers"
import useSnack from "@/use/use-snack"
import flashcardDecksApi, { FlashcardDeck } from "@/api/flashcard-decks-api"
const flashcardDeck = ref<Partial<FlashcardDeck>>({})
const showDialog = useRouteQuery<string, boolean>("showFlashcardDeckCreateDialog", "false", {
transform: booleanTransformer,
})
function show() {
flashcardDeck.value = {}
showDialog.value = true
}
function close() {
showDialog.value = false
}
const emit = defineEmits<{ created: [flashcardDeckId: number] }>()
const isCreating = ref(false)
const formRef = ref<InstanceType<typeof VForm> | null>(null)
const snack = useSnack()
async function validateAndCreate() {
if (formRef.value === null) return
const { valid } = await formRef.value.validate()
if (!valid) return
try {
isCreating.value = true
const { flashcardDeck: newFlashcardDeck } = await flashcardDecksApi.create(flashcardDeck.value)
emit("created", newFlashcardDeck.id)
close()
snack.success("Flashcard Deck Created")
} catch (error) {
console.log(error)
snack.error("Failed to create Flashcard Deck")
} finally {
isCreating.value = false
}
}
defineExpose({
show,
close,
})
</script>
@@ -0,0 +1,30 @@
<template>
<v-btn
class="ml-3"
color="primary"
prepend-icon="mdi-book-open-blank-variant-outline"
text="Start Review"
@click="startReview"
/>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router"
const props = defineProps<{ flashcardDeckId: number }>()
const router = useRouter()
function startReview() {
goToReviewPage()
}
function goToReviewPage() {
router.push({
name: "FlashcardDeckReviewPage",
params: {
flashcardDeckId: props.flashcardDeckId,
},
})
}
</script>
@@ -0,0 +1,205 @@
<template>
<div>
<div class="d-flex align-center justify-space-between mb-1">
<span class="title">Decks</span>
</div>
<div
v-if="isLoading"
class="d-flex justify-center py-4"
>
<v-progress-circular
indeterminate
size="24"
/>
</div>
<template v-else>
<div
v-if="tree.length === 0"
class="text-center py-6 text-body-2 text-medium-emphasis"
>
No decks yet. Create one to get started.
</div>
<draggable
v-model="tree"
:group="{ name: 'decks' }"
item-key="id"
@change="onRootChange"
>
<template #item="{ element }">
<FlashcardDeckTreeNode
v-model:children="element.children"
:deck="element"
:selected-id="selectedId"
@select="onSelect"
@move="handleMove"
@add-child="openCreateDialog"
/>
</template>
</draggable>
</template>
<v-btn
class="ml-2"
variant="text"
prepend-icon="mdi-plus"
text="Add Deck"
@click="openCreateDialog(null)"
/>
<v-dialog
v-model="showCreateDialog"
width="400"
>
<v-form
ref="formRef"
@submit.prevent="createDeck"
>
<v-card>
<v-card-title>{{ activeParentId !== null ? "Add Sub-Deck" : "Add Deck" }}</v-card-title>
<v-card-text>
<v-text-field
v-model="newDeckName"
label="Name"
autofocus
:rules="[required]"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
text="Cancel"
@click="showCreateDialog = false"
/>
<v-btn
type="submit"
color="primary"
variant="flat"
:loading="isCreating"
text="Create"
/>
</v-card-actions>
</v-card>
</v-form>
</v-dialog>
</div>
</template>
<script lang="ts">
import type { FlashcardDeck } from "@/api/flashcard-decks-api"
export type DeckNode = FlashcardDeck & { children: DeckNode[] }
</script>
<script setup lang="ts">
import { ref, watch } from "vue"
import draggable from "vuedraggable"
import { VForm } from "vuetify/components"
import flashcardDecksApi from "@/api/flashcard-decks-api"
import { required } from "@/utils/validators"
import useFlashcardDecks from "@/use/use-flashcard-decks"
import useSnack from "@/use/use-snack"
import FlashcardDeckTreeNode from "@/components/flashcard-decks/FlashcardDeckTreeNode.vue"
const { flashcardDecks, isLoading, fetch } = useFlashcardDecks()
const tree = ref<DeckNode[]>([])
function buildTreeOptimized(decks: FlashcardDeck[]): DeckNode[] {
const map = new Map<number, DeckNode>()
const roots: DeckNode[] = []
decks.forEach((d) => map.set(d.id, { ...d, children: [] }))
decks.forEach((d) => {
const node = map.get(d.id)!
if (d.parentDeckId === null) {
roots.push(node)
} else {
const parent = map.get(d.parentDeckId)
if (parent) parent.children.push(node)
}
})
return roots
}
watch(
flashcardDecks,
(newDecks) => {
tree.value = buildTreeOptimized(newDecks)
},
{ immediate: true, deep: true }
)
// --- Selection ---
const selectedId = ref<number | null>(null)
const emit = defineEmits<{ select: [deck: DeckNode] }>()
function onSelect(deck: DeckNode) {
selectedId.value = deck.id
emit("select", deck)
}
// --- Drag & Drop ---
const snack = useSnack()
async function handleMove({ deckId, newParentId }: { deckId: number; newParentId: number | null }) {
try {
await flashcardDecksApi.update(deckId, { parentDeckId: newParentId })
// await fetch()
} catch {
snack.error("Failed to move deck")
tree.value = buildTreeOptimized(flashcardDecks.value)
}
}
function onRootChange(event: { added?: { element: DeckNode } }) {
if (event.added) {
handleMove({ deckId: event.added.element.id, newParentId: null })
}
}
// --- Create ---
const showCreateDialog = ref(false)
const activeParentId = ref<number | null>(null)
const newDeckName = ref("")
const isCreating = ref(false)
const formRef = ref<InstanceType<typeof VForm> | null>(null)
function openCreateDialog(parentId: number | null) {
activeParentId.value = parentId
newDeckName.value = ""
showCreateDialog.value = true
}
async function createDeck() {
if (!formRef.value) return
const { valid } = await formRef.value.validate()
if (!valid) return
try {
isCreating.value = true
await flashcardDecksApi.create({
name: newDeckName.value,
parentDeckId: activeParentId.value,
})
showCreateDialog.value = false
await fetch()
} catch {
snack.error("Failed to create deck")
} finally {
isCreating.value = false
}
}
defineExpose({ refresh: fetch })
</script>
@@ -0,0 +1,126 @@
<template>
<div>
<div
class="deck-node d-flex align-center px-3 py-2 rounded"
:class="{ selected: isSelected }"
@click="$emit('select', deck)"
>
<v-btn
icon
variant="text"
density="comfortable"
size="default"
class="mr-1"
@click.stop="expanded = !expanded"
>
<v-icon size="default">
{{
hasChildren ? (expanded ? "mdi-chevron-down" : "mdi-chevron-right") : "mdi-circle-small"
}}
</v-icon>
</v-btn>
<v-icon
size="default"
class="mr-2 text-medium-emphasis"
:icon="expanded && hasChildren ? 'mdi-folder-open-outline' : 'mdi-folder-outline'"
/>
<span
class="flex-grow-1 text-body-1"
:class="{ 'font-weight-medium': isSelected }"
>
{{ deck.name }}
</span>
<v-btn
icon
variant="text"
density="comfortable"
size="default"
title="Add sub-deck"
class="add-btn"
@click.stop="$emit('add-child', deck.id)"
>
<v-icon
size="default"
icon="mdi-plus"
/>
</v-btn>
</div>
<div
v-show="expanded"
class="pl-5"
>
<draggable
v-model="children"
:group="{ name: 'decks' }"
item-key="id"
@change="onChange"
>
<template #item="{ element }">
<FlashcardDeckTreeNode
v-model:children="element.children"
:deck="element"
:selected-id="selectedId"
@select="$emit('select', $event)"
@move="$emit('move', $event)"
@add-child="$emit('add-child', $event)"
/>
</template>
</draggable>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue"
import draggable from "vuedraggable"
import type { DeckNode } from "@/components/flashcard-decks/FlashcardDeckTree.vue"
const props = defineProps<{
deck: DeckNode
selectedId?: number | null
}>()
const children = defineModel<DeckNode[]>("children", { required: true })
const emit = defineEmits<{
select: [deck: DeckNode]
move: [payload: { deckId: number; newParentId: number | null }]
"add-child": [parentId: number]
}>()
const expanded = ref(false)
const isSelected = computed(() => props.selectedId === props.deck.id)
const hasChildren = computed(() => children.value.length > 0)
function onChange(event: { added?: { element: DeckNode } }) {
if (event.added) {
emit("move", { deckId: event.added.element.id, newParentId: props.deck.id })
}
}
</script>
<style scoped>
.deck-node {
cursor: grab;
user-select: none;
}
.deck-node:hover {
background: rgb(var(--v-theme-hoverColor));
}
.deck-node.selected {
background: rgb(var(--v-theme-lightprimary));
}
.add-btn {
opacity: 0;
transition: opacity 0.15s;
}
.deck-node:hover .add-btn {
opacity: 1;
}
</style>
@@ -0,0 +1,100 @@
<template>
<v-data-table-server
v-model:items-per-page="perPage"
v-model:page="page"
v-model:sort-by="sortBy"
:headers="headers"
:items="flashcardDecks"
:items-length="totalCount"
:loading="isLoading"
@click:row="rowClicked"
@update:page="updatePage"
>
<template
v-for="(_, name) in $slots"
:key="name"
#[name]="slotProps"
>
<slot
:name="name"
v-bind="slotProps"
></slot>
</template>
</v-data-table-server>
</template>
<script lang="ts">
export const DEFAULT_HEADERS = [
{ title: "Name", key: "name", align: "start" as const, sortable: true },
{ title: "Created At", key: "createdAt", align: "start" as const, sortable: true },
]
</script>
<script setup lang="ts">
import { computed } from "vue"
import useVuetifySortByToSafeRouteQuery, {
type SortItem,
} from "@/use/utils/use-vuetify-sort-by-to-safe-route-query"
import useVuetifySortByToSequelizeSafeOrder from "@/use/utils/use-vuetify-sort-by-to-sequelize-safe-order"
import useRouteQueryPagination from "@/use/utils/use-route-query-pagination"
import useFlashcardDecks, {
type FlashcardDeck,
type FlashcardDeckFiltersOptions,
type FlashcardDeckQueryOptions,
type FlashcardDeckWhereOptions,
} from "@/use/use-flashcard-decks"
const props = withDefaults(
defineProps<{
headers?: { title: string; key: string }[]
filters?: FlashcardDeckFiltersOptions
where?: FlashcardDeckWhereOptions
sortBy?: SortItem[]
routeQuerySuffix?: string
waiting?: boolean
}>(),
{
headers: () => DEFAULT_HEADERS,
filters: () => ({}),
where: () => ({}),
sortBy: () => [],
routeQuerySuffix: "FlashcardDecks",
waiting: false,
}
)
const { page, perPage } = useRouteQueryPagination({ routeQuerySuffix: props.routeQuerySuffix })
const sortBy = useVuetifySortByToSafeRouteQuery(`sortBy${props.routeQuerySuffix}`, props.sortBy)
const order = useVuetifySortByToSequelizeSafeOrder(sortBy)
const queryOptions = computed<FlashcardDeckQueryOptions>(() => ({
where: props.where,
filters: props.filters,
order: order.value,
page: page.value,
perPage: perPage.value,
}))
const { flashcardDecks, totalCount, isLoading, refresh } = useFlashcardDecks(queryOptions, {
skipWatchIf: () => props.waiting,
})
type DeckTableRow = {
item: FlashcardDeck
}
const emit = defineEmits<{ clicked: [deck: FlashcardDeck] }>()
function rowClicked(_event: unknown, row: DeckTableRow) {
emit("clicked", row.item)
}
function updatePage(newPage: number) {
if (isLoading.value || props.waiting) return
page.value = newPage
}
defineExpose({ refresh, totalCount })
</script>
@@ -0,0 +1,27 @@
<template>
<v-card class="pa-5">
<div
class="face-content text-h5 text-center"
v-html="renderMarkdown(flashcard.front)"
/>
</v-card>
</template>
<script setup lang="ts">
import { type Flashcard } from "@/api/flashcards-api"
import renderMarkdown from "@/utils/render-markdown"
defineProps<{ flashcard: Flashcard }>()
</script>
<style scoped>
.face-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1.5;
white-space: pre-line;
}
</style>
@@ -0,0 +1,104 @@
<template>
<v-dialog
v-model="showDialog"
width="500"
>
<v-form
ref="formRef"
@submit.prevent="validateAndCreate"
>
<v-card>
<v-card-title>Add Flashcard</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12">
<v-textarea
v-model="flashcard.front"
label="Front"
rows="3"
:rules="[required]"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="flashcard.back"
label="Back"
rows="3"
:rules="[required]"
/>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
text="Cancel"
@click="close"
/>
<v-btn
type="submit"
color="primary"
variant="flat"
:loading="isCreating"
text="Create"
/>
</v-card-actions>
</v-card>
</v-form>
</v-dialog>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { VForm } from "vuetify/components"
import { required } from "@/utils/validators"
import useSnack from "@/use/use-snack"
import flashcardsApi, { type Flashcard } from "@/api/flashcards-api"
const props = defineProps<{
flashcardDeckId: number
}>()
const emit = defineEmits<{ created: [flashcardId: number] }>()
const showDialog = ref(false)
const isCreating = ref(false)
const formRef = ref<InstanceType<typeof VForm> | null>(null)
const flashcard = ref<Partial<Flashcard>>({})
const snack = useSnack()
function show() {
flashcard.value = {}
showDialog.value = true
}
function close() {
showDialog.value = false
}
async function validateAndCreate() {
if (!formRef.value) return
const { valid } = await formRef.value.validate()
if (!valid) return
try {
isCreating.value = true
const { flashcard: newFlashcard } = await flashcardsApi.create({
...flashcard.value,
flashcardDeckId: props.flashcardDeckId,
})
emit("created", newFlashcard.id)
close()
snack.success("Flashcard created")
} catch {
snack.error("Failed to create flashcard")
} finally {
isCreating.value = false
}
}
defineExpose({ show, close })
</script>
@@ -0,0 +1,130 @@
<template>
<div class="flashcard-scene">
<div
class="flashcard"
:class="{ 'is-flipped': isFlipped }"
@click="flip"
>
<!-- Front -->
<v-card
class="flashcard-face flashcard-face--front"
rounded="xl"
elevation="4"
>
<div class="face-label text-textSecondary text-caption text-uppercase font-weight-bold">
Question
</div>
<div
class="face-content text-h5 text-center"
v-html="renderMarkdown(flashcard.front)"
/>
<div class="face-hint text-textSecondary text-caption">Click to reveal</div>
</v-card>
<!-- Back -->
<v-card
class="flashcard-face flashcard-face--back"
color="white"
rounded="xl"
elevation="4"
>
<div class="face-label text-textSecondary text-caption text-uppercase font-weight-bold">
Answer
</div>
<div
class="face-content text-h5 text-center"
v-html="renderMarkdown(flashcard.back)"
/>
</v-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { type Flashcard } from "@/api/flashcards-api"
import renderMarkdown from "@/utils/render-markdown"
defineProps<{ flashcard: Flashcard }>()
const isFlipped = ref(false)
function flip() {
isFlipped.value = !isFlipped.value
}
</script>
<style scoped>
.flashcard-scene {
perspective: 1200px;
width: 100%;
max-width: 640px;
height: 380px;
margin: 0 auto;
}
.flashcard {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.flashcard.is-flipped {
transform: rotateY(180deg);
}
.flashcard-face {
position: absolute;
inset: 0;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 48px;
gap: 16px;
}
.flashcard-face--back {
transform: rotateY(180deg);
color: black !important;
}
.flashcard-face--back :deep(*) {
color: black !important;
}
.face-label {
position: absolute;
top: 20px;
left: 28px;
letter-spacing: 0.08em;
}
.face-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1.5;
white-space: pre-line;
}
.face-hint {
position: absolute;
bottom: 20px;
}
.face-actions {
position: absolute;
bottom: 20px;
display: flex;
gap: 12px;
}
</style>
@@ -0,0 +1,107 @@
<template>
<v-data-table-server
v-model:items-per-page="perPage"
v-model:page="page"
v-model:sort-by="sortBy"
:headers="headers"
:items="flashcards"
:items-length="totalCount"
:loading="isLoading"
@click:row="rowClicked"
@update:page="updatePage"
>
<template #item.front="{ item }">
<span class="text-truncate d-inline-block">{{ item.front }}</span>
</template>
<template #item.back="{ item }">
<span class="text-truncate d-inline-block">{{ item.back }}</span>
</template>
<template
v-for="(_, name) in $slots"
:key="name"
#[name]="slotProps"
>
<slot
:name="name"
v-bind="slotProps"
></slot>
</template>
</v-data-table-server>
</template>
<script lang="ts">
export const DEFAULT_HEADERS = [
{ title: "Front", key: "front", align: "start" as const, sortable: false },
{ title: "Back", key: "back", align: "start" as const, sortable: false },
{ title: "Created At", key: "createdAt", align: "start" as const, sortable: true },
]
</script>
<script setup lang="ts">
import { computed } from "vue"
import useVuetifySortByToSafeRouteQuery, {
type SortItem,
} from "@/use/utils/use-vuetify-sort-by-to-safe-route-query"
import useVuetifySortByToSequelizeSafeOrder from "@/use/utils/use-vuetify-sort-by-to-sequelize-safe-order"
import useRouteQueryPagination from "@/use/utils/use-route-query-pagination"
import useFlashcards, {
type Flashcard,
type FlashcardFiltersOptions,
type FlashcardQueryOptions,
type FlashcardWhereOptions,
} from "@/use/use-flashcards"
const props = withDefaults(
defineProps<{
headers?: { title: string; key: string }[]
filters?: FlashcardFiltersOptions
where?: FlashcardWhereOptions
sortBy?: SortItem[]
routeQuerySuffix?: string
waiting?: boolean
}>(),
{
headers: () => DEFAULT_HEADERS,
filters: () => ({}),
where: () => ({}),
sortBy: () => [],
routeQuerySuffix: "Flashcards",
waiting: false,
}
)
const { page, perPage } = useRouteQueryPagination({ routeQuerySuffix: props.routeQuerySuffix })
const sortBy = useVuetifySortByToSafeRouteQuery(`sortBy${props.routeQuerySuffix}`, props.sortBy)
const order = useVuetifySortByToSequelizeSafeOrder(sortBy)
const queryOptions = computed<FlashcardQueryOptions>(() => ({
where: props.where,
filters: props.filters,
order: order.value,
page: page.value,
perPage: perPage.value,
}))
const { flashcards, totalCount, isLoading, refresh } = useFlashcards(queryOptions, {
skipWatchIf: () => props.waiting,
})
type FlashcardTableRow = {
item: Flashcard
}
const emit = defineEmits<{ clicked: [flashcard: Flashcard] }>()
function rowClicked(_event: unknown, row: FlashcardTableRow) {
emit("clicked", row.item)
}
function updatePage(newPage: number) {
if (isLoading.value || props.waiting) return
page.value = newPage
}
defineExpose({ refresh, totalCount })
</script>
@@ -9,6 +9,20 @@
<AppLogo /> <AppLogo />
</div> </div>
<v-tabs
class="ml-2"
color="primary"
>
<v-tab
:to="{ name: 'DashboardPage' }"
text="Dashboard"
/>
<v-tab
:to="{ name: 'FlashcardsPage' }"
text="Flashcards"
/>
</v-tabs>
<v-spacer /> <v-spacer />
<!-- TODO Add notifications --> <!-- TODO Add notifications -->
+118
View File
@@ -0,0 +1,118 @@
<template>
<v-row class="fill-height">
<v-col
cols="12"
md="3"
>
<v-card
height="100%"
class="pa-2"
>
<FlashcardDeckTree
ref="deckTree"
@select="onDeckSelected"
/>
</v-card>
</v-col>
<v-col
cols="12"
md="9"
>
<template v-if="selectedDeck">
<div class="d-flex align-center justify-space-between mb-4">
<v-spacer />
<v-btn
color="primary"
variant="tonal"
prepend-icon="mdi-plus"
text="Add Flashcard"
@click="openFlashcardCreateDialog"
/>
<FlashcardDeckStartReviewBtn :flashcard-deck-id="selectedDeck.id" />
</div>
<v-divider class="my-5" />
<v-row>
<v-col
v-for="flashcard in flashcards"
:key="flashcard.id"
cols="12"
md="6"
lg="4"
>
<FlashcardCard :flashcard="flashcard" />
</v-col>
</v-row>
<EnhancedPagination
v-model="page"
v-model:per-page="perPage"
:total-count="totalCount"
class="mt-4"
/>
<FlashcardCreateDialog
v-if="selectedDeck"
ref="flashcardCreateDialog"
:flashcard-deck-id="selectedDeck.id"
@created="refreshFlashcards"
/>
</template>
<div
v-else
class="d-flex align-center justify-center h-100 text-medium-emphasis"
>
Select a deck to view its flashcards
</div>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import useBreadcrumbs from "@/use/use-breadcrumbs"
import useFlashcards from "@/use/use-flashcards"
import useRouteQueryPagination from "@/use/utils/use-route-query-pagination"
import FlashcardDeckTree, {
type DeckNode,
} from "@/components/flashcard-decks/FlashcardDeckTree.vue"
import FlashcardCard from "@/components/flashcards/FlashcardCard.vue"
import FlashcardCreateDialog from "@/components/flashcards/FlashcardCreateDialog.vue"
import FlashcardDeckStartReviewBtn from "@/components/flashcard-decks/FlashcardDeckStartReviewBtn.vue"
import EnhancedPagination from "@/components/common/EnhancedPagination.vue"
const deckTree = ref<InstanceType<typeof FlashcardDeckTree> | null>(null)
const selectedDeck = ref<DeckNode | null>(null)
const flashcardCreateDialog = ref<InstanceType<typeof FlashcardCreateDialog> | null>(null)
const { page, perPage } = useRouteQueryPagination({ perPage: 4 })
const flashcardsQueryOptions = computed(() => ({
where: { flashcardDeckId: selectedDeck.value?.id },
page: page.value,
perPage: perPage.value,
}))
const {
flashcards,
totalCount,
refresh: refreshFlashcards,
} = useFlashcards(flashcardsQueryOptions, {
skipWatchIf: () => selectedDeck.value === null,
})
function onDeckSelected(deck: DeckNode) {
selectedDeck.value = deck
}
function openFlashcardCreateDialog() {
flashcardCreateDialog.value?.show()
}
useBreadcrumbs("Study")
</script>
@@ -0,0 +1,184 @@
<template>
<div>
<div class="d-flex">
<v-spacer />
<v-btn
class="mt-2"
variant="tonal"
prepend-icon="mdi-shuffle"
text="Shuffle"
/>
</div>
<v-skeleton-loader
v-if="isLoading"
type="card"
/>
<div v-else-if="isEmpty(flashcards)"></div>
<div
v-else-if="isEmpty(queue)"
class="review-layout mt-10"
>
<v-icon
size="64"
color="success"
>
mdi-check-circle-outline
</v-icon>
<div class="text-h5">Done!</div>
<div class="text-medium-emphasis">
{{ numberOfCorrect }} correct &bull; {{ numberOfIncorrect }} incorrect
</div>
<div class="text-medium-emphasis text-caption">
{{ elapsedTime }}
</div>
</div>
<div
v-else
class="review-layout"
>
<FlashcardReviewCard
:key="queue[currentIndex].id"
class="mt-5"
:flashcard="queue[currentIndex]"
/>
<div class="review-nav">
<v-btn
icon="mdi-chevron-left"
variant="text"
size="x-large"
:disabled="currentIndex === 0"
@click="prev"
/>
<v-btn
class="mr-3"
color="error"
variant="tonal"
prepend-icon="mdi-close"
@click="markCurrentCardIncorrect"
>
{{ numberOfIncorrect }}
</v-btn>
<v-btn
class="ml-3"
color="success"
variant="tonal"
append-icon="mdi-check"
@click="markCurrentCardCorrect"
>
{{ numberOfCorrect }}
</v-btn>
<v-btn
icon="mdi-chevron-right"
variant="text"
size="x-large"
:disabled="currentIndex === queue.length - 1"
@click="next"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { isEmpty, isNil } from "lodash"
import { computed, ref, watch } from "vue"
import useFlashcardDeck from "@/use/use-flashcard-deck"
import useFlashcards, { type Flashcard, FlashcardQueryOptions } from "@/use/use-flashcards"
import FlashcardReviewCard from "@/components/flashcards/FlashcardReviewCard.vue"
const props = defineProps<{ flashcardDeckId: string }>()
const flashcardDeckIdAsNumber = computed(() => parseInt(props.flashcardDeckId))
const { flashcardDeck } = useFlashcardDeck(flashcardDeckIdAsNumber)
const flashcardsQueryOptions = computed<FlashcardQueryOptions>(() => {
return {
where: {
flashcardDeckId: flashcardDeck.value?.id,
},
}
})
const { flashcards, isLoading } = useFlashcards(flashcardsQueryOptions, {
skipWatchIf: () => isNil(flashcardDeck.value),
})
const currentIndex = ref(0)
const queue = ref<Flashcard[]>([])
const results = ref(new Map<number, "correct" | "incorrect">())
const startTime = ref<number | null>(null)
const endTime = ref<number | null>(null)
const elapsedTime = computed(() => {
if (startTime.value === null || endTime.value === null) return ""
const seconds = Math.floor((endTime.value - startTime.value) / 1000)
const m = Math.floor(seconds / 60)
const s = seconds % 60
return m > 0 ? `${m}m ${s}s` : `${s}s`
})
watch(
flashcards,
(newFlashcards) => {
queue.value = [...newFlashcards]
if (newFlashcards.length > 0) startTime.value = Date.now()
},
{ immediate: true }
)
const numberOfCorrect = computed(
() => [...results.value.values()].filter((r) => r === "correct").length
)
const numberOfIncorrect = computed(
() => [...results.value.values()].filter((r) => r === "incorrect").length
)
function next() {
if (currentIndex.value < queue.value.length - 1) {
currentIndex.value++
}
}
function prev() {
if (currentIndex.value > 0) {
currentIndex.value--
}
}
function markCurrentCard(result: "correct" | "incorrect") {
const card = queue.value[currentIndex.value]
results.value = new Map(results.value).set(card.id, result)
queue.value.splice(currentIndex.value, 1)
if (queue.value.length === 0) {
endTime.value = Date.now()
} else if (currentIndex.value >= queue.value.length) {
currentIndex.value--
}
}
function markCurrentCardCorrect() {
markCurrentCard("correct")
}
function markCurrentCardIncorrect() {
markCurrentCard("incorrect")
}
</script>
<style scoped>
.review-layout {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.review-nav {
display: flex;
align-items: center;
gap: 8px;
}
</style>
+17
View File
@@ -34,6 +34,23 @@ const routes: RouteRecordRaw[] = [
title: "Dashboard", title: "Dashboard",
}, },
}, },
{
name: "FlashcardsPage",
path: "flashcards",
component: () => import("@/pages/FlashcardsPage.vue"),
meta: {
title: "Flashcards",
},
},
{
name: "FlashcardDeckReviewPage",
path: "flashcard-deck/:flashcardDeckId/review",
component: () => import("@/pages/flashcard-deck-reviews/FlashcardDeckReviewPage.vue"),
props: true,
meta: {
title: "Deck review",
},
},
{ {
path: "", path: "",
component: () => import("@/layouts/LayoutWithBreadcrumbs.vue"), component: () => import("@/layouts/LayoutWithBreadcrumbs.vue"),
+95 -107
View File
@@ -1,204 +1,192 @@
@use "./variables" as *; @use "./variables" as *;
html { html {
.bg-success { .bg-success {
color: $white !important; color: $white !important;
} }
.bg-primary { .bg-primary {
color: $white !important; color: $white !important;
} }
.bg-secondary { .bg-secondary {
color: $white !important; color: $white !important;
} }
.bg-warning { .bg-warning {
color: $white !important; color: $white !important;
} }
.bg-secondary-gradient { .bg-secondary-gradient {
background: linear-gradient(287deg, rgb(var(--v-theme-primary)) .54%, #1bcaff 100.84%); background: linear-gradient(287deg, rgb(var(--v-theme-primary)) 0.54%, #1bcaff 100.84%);
} }
} }
.border, .border,
.v-divider { .v-divider {
border-color: rgba(var(--v-border-color)) !important; border-color: rgba(var(--v-border-color)) !important;
} }
.avtar-border { .avtar-border {
border: 2px solid rgb(var(--v-theme-surface)) !important; border: 2px solid rgb(var(--v-theme-surface)) !important;
} }
.subtext { .subtext {
font-size: $font-size-root; font-size: $font-size-root;
line-height: 1.75rem; line-height: 1.75rem;
} }
.v-dialog { .v-dialog {
&.dialog-mw { &.dialog-mw {
max-width: 800px; max-width: 800px;
} }
} }
.round-40 { .round-40 {
height: 40px; height: 40px;
width: 40px; width: 40px;
} }
.round-56 { .round-56 {
height: 56px; height: 56px;
width: 56px; width: 56px;
} }
.round-48 { .round-48 {
height: 48px; height: 48px;
width: 48px; width: 48px;
} }
.round-30 { .round-30 {
height: 30px; height: 30px;
width: 30px; width: 30px;
} }
.lh-0 { .lh-0 {
line-height: 0 !important; line-height: 0 !important;
} }
.lh-28 { .lh-28 {
line-height: 28px !important; line-height: 28px !important;
} }
.lh-32 { .lh-32 {
line-height: 32px !important; line-height: 32px !important;
} }
.space-p-96 { .space-p-96 {
padding: 96px 0 !important; padding: 96px 0 !important;
} }
.ps-96 { .ps-96 {
padding-inline-start: 96px !important; padding-inline-start: 96px !important;
} }
.pt-96 { .pt-96 {
padding-top: 96px !important; padding-top: 96px !important;
} }
.end-0 { .end-0 {
inset-inline-end: 0; inset-inline-end: 0;
} }
.top-0 { .top-0 {
top: 0; top: 0;
} }
.no-scrollbar { .no-scrollbar {
height: calc(100vh - 350px); height: calc(100vh - 350px);
} }
.msg-chat-height { .msg-chat-height {
height: calc(-500px + 100vh); height: calc(-500px + 100vh);
} }
@media screen and (max-width:991px) { @media screen and (max-width: 991px) {
.overflow-x-reposive { .overflow-x-reposive {
overflow-x: scroll; overflow-x: scroll;
overflow-y: hidden; overflow-y: hidden;
} }
.border-m-none { .border-m-none {
border: 0 !important border: 0 !important;
} }
} }
@media screen and (max-height:767px) { @media screen and (max-height: 767px) {
.msg-chat-height { .msg-chat-height {
height: calc(-315px + 100vh); height: calc(-315px + 100vh);
} }
} }
.max-h-600 { .max-h-600 {
max-height: 600px; max-height: 600px;
height: calc(100vh - 100px); height: calc(100vh - 100px);
} }
.custom-hover-primary { .custom-hover-primary {
.iconify {
color: rgb(255, 255, 255) !important;
@media screen and (max-width: 991px) {
color: rgba(var(--v-theme-textPrimary), 0.8) !important;
}
}
&:hover {
background-color: rgba(var(--v-theme-lightprimary), 0.1);
.iconify { .iconify {
color: rgb(255, 255, 255) !important; @media screen and (max-width: 991px) {
color: rgb(var(--v-theme-primary)) !important;
@media screen and (max-width:991px) { }
color: rgba(var(--v-theme-textPrimary), 0.8) !important
}
}
&:hover {
background-color: rgba(var(--v-theme-lightprimary), 0.1);
.iconify {
@media screen and (max-width:991px) {
color: rgb(var(--v-theme-primary)) !important
}
}
} }
}
} }
.custom-hover-primary-white { .custom-hover-primary-white {
.iconify { .iconify {
color: rgb(255, 255, 255) !important; color: rgb(255, 255, 255) !important;
}
&:hover {
} background-color: rgba(var(--v-theme-lightprimary), 0.1);
}
&:hover {
background-color: rgba(var(--v-theme-lightprimary), 0.1);
}
} }
.no-icon { .no-icon {
.v-input__prepend,
.v-input__prepend, .v-input__append {
.v-input__append { display: none !important;
display: none !important; }
}
} }
.bg-white { .bg-white {
background-color: rgb(255, 255, 255) !important; background-color: rgb(255, 255, 255) !important;
} }
.v-badge { .v-badge {
&.x-small-badge { &.x-small-badge {
.v-badge__badge { .v-badge__badge {
height: 6px !important; height: 6px !important;
width: 6px !important; width: 6px !important;
}
} }
}
} }
.one-line { .one-line {
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
} }
.two-line { .two-line {
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
} }
.z-1 { .z-1 {
z-index: 1; z-index: 1;
} }
+44 -44
View File
@@ -1,48 +1,48 @@
.v-selection-control.v-selection-control--density-default { .v-selection-control.v-selection-control--density-default {
.v-switch__track, .v-switch__track,
.v-switch__thumb { .v-switch__thumb {
background-color: rgb(var(--v-theme-grey200)); background-color: rgb(var(--v-theme-grey200));
}
&.v-selection-control--dirty {
.v-selection-control__wrapper.text-primary {
.v-switch__track {
background-color: rgba(var(--v-theme-primary), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-primary));
}
} }
&.v-selection-control--dirty { .v-selection-control__wrapper.text-secondary {
.v-selection-control__wrapper.text-primary { .v-switch__track {
.v-switch__track { background-color: rgba(var(--v-theme-secondary), 0.6);
background-color: rgba(var(--v-theme-primary), 0.6); }
} .v-switch__thumb {
.v-switch__thumb { background-color: rgb(var(--v-theme-secondary));
background-color: rgb(var(--v-theme-primary)); }
}
}
.v-selection-control__wrapper.text-secondary {
.v-switch__track {
background-color: rgba(var(--v-theme-secondary), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-secondary));
}
}
.v-selection-control__wrapper.text-warning {
.v-switch__track {
background-color: rgba(var(--v-theme-warning), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-warning));
}
}
.v-selection-control__wrapper.text-error {
.v-switch__track {
background-color: rgba(var(--v-theme-error), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-error));
}
}
.v-selection-control__wrapper.text-success {
.v-switch__track {
background-color: rgba(var(--v-theme-success), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-success));
}
}
} }
.v-selection-control__wrapper.text-warning {
.v-switch__track {
background-color: rgba(var(--v-theme-warning), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-warning));
}
}
.v-selection-control__wrapper.text-error {
.v-switch__track {
background-color: rgba(var(--v-theme-error), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-error));
}
}
.v-selection-control__wrapper.text-success {
.v-switch__track {
background-color: rgba(var(--v-theme-success), 0.6);
}
.v-switch__thumb {
background-color: rgb(var(--v-theme-success));
}
}
}
} }
+68 -68
View File
@@ -1,81 +1,81 @@
// theme : dark // theme : dark
div[class*='v-theme--DARK_'] { div[class*="v-theme--DARK_"] {
.smallCap { .smallCap {
color: rgb(var(--v-theme-textSecondary)); color: rgb(var(--v-theme-textSecondary));
}
.elevation-10 {
box-shadow:
rgb(145 158 171 / 30%) 0px 0px 2px 0px,
rgb(145 158 171 / 2%) 0px 12px 24px -4px !important;
}
.v-field__outline {
--v-field-border-opacity: 0.38 !important;
}
.front-wraper {
.bg-background {
background-color: rgb(var(--v-theme-hoverColor)) !important;
} }
.elevation-10 { .front-dark {
box-shadow: rgb(145 158 171 / 30%) 0px 0px 2px 0px, rgb(145 158 171 / 2%) 0px 12px 24px -4px !important; &.bg-textPrimary {
background-color: rgb(var(--v-theme-surface)) !important;
}
} }
.v-field__outline{ .bg-textPrimary {
--v-field-border-opacity: 0.38 !important; background-color: rgb(var(--v-theme-textSecondary)) !important;
}
}
#vector-map .dxm-layers path {
fill: #7c8fac !important;
}
.svgMap-map-wrapper {
.svgMap-country {
stroke: #878585;
fill: #1a2537 !important;
&#svgMap-map-country-IN {
fill: rgb(var(--v-theme-secondary)) !important;
}
&#svgMap-map-country-AF {
fill: rgb(var(--v-theme-purple)) !important;
}
&#svgMap-map-country-US {
fill: rgb(var(--v-theme-primary)) !important;
}
} }
.front-wraper{ .svgMap-map-controls-zoom {
.bg-background{ background: #c9d6de !important;
background-color: rgb(var(--v-theme-hoverColor)) !important;
}
.front-dark{
&.bg-textPrimary{
background-color: rgb(var(--v-theme-surface)) !important;
}
}
.bg-textPrimary{
background-color: rgb(var(--v-theme-textSecondary)) !important;
}
} }
.svgMap-control-button {
background-color: rgb(var(--v-theme-textSecondary)) !important;
#vector-map .dxm-layers path {
fill: #7C8FAC !important;
} }
}
.dark-card-title {
color: rgb(var(--v-theme-surface));
}
.svgMap-map-wrapper { .fc {
.svgMap-country { .fc-button-primary:not(:disabled).fc-button-active {
stroke: #878585; background-color: rgb(var(--v-theme-grey100));
fill: #1A2537 !important;
&#svgMap-map-country-IN {
fill: rgb(var(--v-theme-secondary)) !important;
}
&#svgMap-map-country-AF {
fill: rgb(var(--v-theme-purple)) !important;
}
&#svgMap-map-country-US {
fill: rgb(var(--v-theme-primary)) !important;
}
}
.svgMap-map-controls-zoom {
background: #c9d6de !important;
}
.svgMap-control-button{
background-color: rgb(var(--v-theme-textSecondary)) !important;
}
} }
.dark-card-title{ .fc-button-group {
color:rgb(var(--v-theme-surface)); > .fc-button {
} &:hover,
&:focus {
.fc{ background-color: rgba(var(--v-theme-grey100));
.fc-button-primary:not(:disabled).fc-button-active { color: #fff;
background-color: rgb(var(--v-theme-grey100)); .fc-icon {
} color: #fff;
.fc-button-group { }
>.fc-button {
&:hover,&:focus{
background-color: rgba(var(--v-theme-grey100));
color: #fff;
.fc-icon{
color: #fff;
}
}
}
} }
}
} }
}
} }
+183 -192
View File
@@ -1,231 +1,222 @@
@use "../variables" as *; @use "../variables" as *;
.horizontalLayout { .horizontalLayout {
.v-main { .v-main {
margin: 0 16px !important; margin: 0 16px !important;
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
margin: 0 10px !important; margin: 0 10px !important;
}
} }
}
} }
.horizontal-header { .horizontal-header {
&.v-app-bar .v-toolbar__content { &.v-app-bar .v-toolbar__content {
padding: 0; padding: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.maxWidth { .maxWidth {
@media screen and (max-width: 1199px) { @media screen and (max-width: 1199px) {
padding: 0 8px !important; padding: 0 8px !important;
}
} }
}
} }
.ddMenu { .ddMenu {
&.ddLevel-1 { &.ddLevel-1 {
.navItem { .navItem {
.navItemLink { .navItemLink {
.dot { .dot {
height: 6px; height: 6px;
width: 6px; width: 6px;
background-color: rgb(var(--v-theme-textSecondary)); background-color: rgb(var(--v-theme-textSecondary));
border-radius: 50%; border-radius: 50%;
margin-inline-end: 8px !important; margin-inline-end: 8px !important;
}
}
&:hover {
.dot {
background-color: rgb(var(--v-theme-secondary));
}
}
} }
} }
&.ddLevel-2 { &:hover {
.navItem { .dot {
.navItemLink { background-color: rgb(var(--v-theme-secondary));
.dot {
height: 6px;
width: 6px;
background-color: rgb(var(--v-theme-textSecondary));
border-radius: 50%;
margin-inline-end: 8px !important;
}
}
&:hover {
.dot {
background-color: rgb(var(--v-theme-secondary));
}
}
} }
}
} }
}
&.ddLevel-2 {
.navItem {
.navItemLink {
.dot {
height: 6px;
width: 6px;
background-color: rgb(var(--v-theme-textSecondary));
border-radius: 50%;
margin-inline-end: 8px !important;
}
}
&:hover {
.dot {
background-color: rgb(var(--v-theme-secondary));
}
}
}
}
} }
.horizontalMenu { .horizontalMenu {
.v-toolbar__content { .v-toolbar__content {
max-width: 1270px; max-width: 1270px;
margin: 0 auto; margin: 0 auto;
}
.navItem:has(.ddMenu.ddLevel-1 li a.router-link-active) {
background-color: rgb(var(--v-theme-secondary)) !important;
border-radius: 9999px;
.navcollapse {
color: rgba(255, 255, 255);
} }
}
.navItem:has(.ddMenu.ddLevel-1 li a.router-link-active) {
background-color: rgb(var(--v-theme-secondary)) !important;
border-radius: 9999px;
.navcollapse {
color: rgba(255, 255, 255);
}
}
} }
.mobile-menu { .mobile-menu {
.v-navigation-drawer { .v-navigation-drawer {
margin-top: -70px !important; margin-top: -70px !important;
height: 100vh !important; height: 100vh !important;
z-index: 2000 !important; z-index: 2000 !important;
} }
} }
@media (min-width: 960px) { @media (min-width: 960px) {
.horizontalMenu { .horizontalMenu {
margin-top: 65px; margin-top: 65px;
margin-bottom: -70px; margin-bottom: -70px;
.maxWidth { .maxWidth {
.horizontal-navbar { .horizontal-navbar {
max-width: 1160px; max-width: 1160px;
} }
} }
}
.horizontal-navbar {
padding: 16px 0;
margin: 0px auto;
align-items: center;
display: flex;
z-index: 11;
font-size: 0.875rem;
position: relative;
ul {
padding: 0px;
margin: 0px;
} }
.horizontal-navbar { .ddMenu {
padding: 16px 0; li {
margin: 0px auto; a {
align-items: center; color: rgb(var(--v-theme-textPrimary)) !important;
}
}
}
li {
list-style: none;
a {
text-decoration: none;
display: flex; display: flex;
z-index: 11; align-items: center;
font-size: 0.875rem; padding: 10px 13px;
position: relative; height: 40px;
ul { .navIcon {
padding: 0px; margin-right: 10px;
margin: 0px; display: flex;
} }
.ddMenu { .ddIcon {
li { margin-top: 2px;
a {
color: rgb(var(--v-theme-textPrimary)) !important;
}
}
} }
li { &.router-link-exact-active {
list-style: none; background-color: transparent;
color: rgba(var(--v-theme-secondary)) !important;
a { .dot {
text-decoration: none; background-color: rgb(var(--v-theme-secondary)) !important;
display: flex; }
align-items: center;
padding: 10px 13px;
height: 40px;
.navIcon {
margin-right: 10px;
display: flex;
}
.ddIcon {
margin-top: 2px;
}
&.router-link-exact-active {
background-color: transparent;
color: rgba(var(--v-theme-secondary)) !important;
.dot {
background-color: rgb(var(--v-theme-secondary)) !important;
}
}
}
}
.navItem {
position: relative;
.single-link {
&:hover {
color: rgb(var(--v-theme-secondary)) !important;
}
}
.ddMenu {
.navItem {
.navcollapse {
&:hover {
color: rgb(var(--v-theme-secondary)) !important;
}
}
}
}
}
.ddMenu {
position: absolute;
width: 230px;
display: none;
top: 40px;
padding: 10px;
z-index: 1;
background-color: rgb(var(--v-theme-surface));
box-shadow: $box-shadow;
border-radius: $border-radius-root;
li {
margin-bottom: 3px;
}
}
.ddLevel-2,
.ddLevel-3 {
top: -5px;
left: 212px;
}
.navItem:hover {
>.ddMenu {
display: block;
}
}
>li:hover {
background-color: rgb(var(--v-theme-lightprimary));
border-radius: 9999px;
>.navItemLink {
color: rgb(var(--v-theme-secondary));
opacity: 1;
}
}
.router-link-exact-active {
color: rgb(var(--v-theme-secondary));
font-weight: 500;
background-color: rgb(var(--v-theme-lightprimary));
border-radius: $border-radius-root;
} }
}
} }
}
.navItem {
position: relative;
.single-link {
&:hover {
color: rgb(var(--v-theme-secondary)) !important;
}
}
.ddMenu {
.navItem {
.navcollapse {
&:hover {
color: rgb(var(--v-theme-secondary)) !important;
}
}
}
}
}
.ddMenu {
position: absolute;
width: 230px;
display: none;
top: 40px;
padding: 10px;
z-index: 1;
background-color: rgb(var(--v-theme-surface));
box-shadow: $box-shadow;
border-radius: $border-radius-root;
li {
margin-bottom: 3px;
}
}
.ddLevel-2,
.ddLevel-3 {
top: -5px;
left: 212px;
}
.navItem:hover {
> .ddMenu {
display: block;
}
}
> li:hover {
background-color: rgb(var(--v-theme-lightprimary));
border-radius: 9999px;
> .navItemLink {
color: rgb(var(--v-theme-secondary));
opacity: 1;
}
}
.router-link-exact-active {
color: rgb(var(--v-theme-secondary));
font-weight: 500;
background-color: rgb(var(--v-theme-lightprimary));
border-radius: $border-radius-root;
}
}
}
+308 -316
View File
@@ -2,373 +2,365 @@
/*This is for the logo*/ /*This is for the logo*/
.leftSidebar { .leftSidebar {
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, .03), 0 0 1px 0 rgba(0, 0, 0, .1); box-shadow:
0 3px 4px 0 rgba(0, 0, 0, 0.03),
0 0 1px 0 rgba(0, 0, 0, 0.1);
.logo { .logo {
padding-left: 7px; padding-left: 7px;
}
.mini-icon {
display: none;
}
.mini-text {
display: block;
}
.profile {
background: url("@/assets/images/backgrounds/user-info.jpg") no-repeat;
}
.profile-name {
background: rgba(0, 0, 0, 0.5);
margin-top: -6px;
height: 35px;
h5 {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
}
.mini-icon { .v-list--density-default .v-list-subheader {
display: none; padding-inline-start: 0 !important;
} }
.mini-text {
display: block;
}
.profile {
background: url("@/assets/images/backgrounds/user-info.jpg") no-repeat;
}
.profile-name {
background: rgba(0, 0, 0, 0.5);
margin-top: -6px;
height: 35px;
h5 {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.v-list--density-default .v-list-subheader {
padding-inline-start: 0 !important;
}
} }
.verticalLayout { .verticalLayout {
.logo { .logo {
width: 250px; width: 250px;
@media screen and (max-width:1024px) { @media screen and (max-width: 1024px) {
width: auto; width: auto;
}
} }
}
} }
/*This is for the Vertical sidebar*/ /*This is for the Vertical sidebar*/
.scrollnavbar { .scrollnavbar {
height: 100%; height: 100%;
.userbottom { .userbottom {
position: fixed; position: fixed;
bottom: 0px; bottom: 0px;
width: 100%; width: 100%;
}
.smallCap {
padding: 3px 12px 12px 0px !important;
font-size: 0.875rem;
font-weight: 500;
margin-top: 24px;
color: rgb(var(--v-theme-textPrimary));
&:first-child {
margin-top: 0 !important;
}
}
/*General Menu css*/
.v-list-group__items .v-list-item,
.v-list-item {
border-radius: $border-radius-root;
padding-inline-start: calc(14px + var(--indent-padding) / 10) !important;
margin: 0 0 2px;
&:hover {
color: rgb(var(--v-theme-secondary));
} }
.smallCap { .v-list-item__prepend {
padding: 3px 12px 12px 0px !important; margin-inline-end: 13px;
font-size: 0.875rem;
font-weight: 500;
margin-top: 24px;
color: rgb(var(--v-theme-textPrimary));
&:first-child {
margin-top: 0 !important;
}
} }
.v-list-item__append {
font-size: 0.875rem;
.v-icon {
margin-inline-start: 13px;
}
}
/*General Menu css*/ .v-list-item-title {
.v-list-group__items .v-list-item, font-size: 0.875rem;
}
}
.v-list-group__items {
.v-list-item { .v-list-item {
border-radius: $border-radius-root; min-height: 35px !important;
padding-inline-start: calc(14px + var(--indent-padding) / 10) !important; padding-inline-start: calc(12px + var(--indent-padding) / 10) !important;
margin: 0 0 2px; .v-list-item__prepend .dot {
height: 6px;
width: 6px;
background-color: rgb(var(--v-theme-textSecondary));
border-radius: 50%;
margin-inline-end: 8px !important;
opacity: 0;
}
.v-list-item-title {
font-size: 14px !important;
}
&:hover {
color: rgb(var(--v-theme-secondary));
&:hover { .v-list-item__prepend .dot {
color: rgb(var(--v-theme-secondary)); background-color: rgb(var(--v-theme-secondary));
} }
}
&.v-list-item--active {
.v-list-item__prepend .dot {
.v-list-item__prepend { background-color: rgb(var(--v-theme-secondary));
margin-inline-end: 13px;
} }
}
}
}
.v-list-item__append { /*This is for the dropdown*/
font-size: 0.875rem; .v-list {
color: rgb(var(--v-theme-textPrimary));
.v-icon { > .v-list-item.v-list-item--active,
margin-inline-start: 13px; .v-list-item--active > .v-list-item__overlay {
} background: rgb(var(--v-theme-secondary));
} color: white;
.v-list-item-title {
font-size: 0.875rem;
}
} }
.v-list-group__items { > .v-list-group {
.v-list-item { position: relative;
min-height: 35px !important;
padding-inline-start: calc(12px + var(--indent-padding) / 10) !important;
.v-list-item__prepend .dot { > .v-list-item--active,
height: 6px; > .v-list-item--active:hover {
width: 6px; background: rgb(var(--v-theme-secondary));
background-color: rgb(var(--v-theme-textSecondary)); color: white;
border-radius: 50%; }
margin-inline-end: 8px !important;
opacity: 0;
}
.v-list-item-title { .v-list-group__items .v-list-item.v-list-item--active,
font-size: 14px !important; .v-list-group__items .v-list-item.v-list-item--active > .v-list-item__overlay {
} background: transparent;
color: rgb(var(--v-theme-secondary));
&:hover { }
color: rgb(var(--v-theme-secondary));
.v-list-item__prepend .dot {
background-color: rgb(var(--v-theme-secondary));
}
}
&.v-list-item--active {
.v-list-item__prepend .dot {
background-color: rgb(var(--v-theme-secondary));
}
}
}
}
/*This is for the dropdown*/
.v-list {
color: rgb(var(--v-theme-textPrimary));
>.v-list-item.v-list-item--active,
.v-list-item--active>.v-list-item__overlay {
background: rgb(var(--v-theme-secondary));
color: white;
}
>.v-list-group {
position: relative;
>.v-list-item--active,
>.v-list-item--active:hover {
background: rgb(var(--v-theme-secondary));
color: white;
}
.v-list-group__items .v-list-item.v-list-item--active,
.v-list-group__items .v-list-item.v-list-item--active>.v-list-item__overlay {
background: transparent;
color: rgb(var(--v-theme-secondary));
}
}
} }
}
} }
.v-navigation-drawer--rail { .v-navigation-drawer--rail {
.scrollnavbar .v-list .v-list-group__items,
.hide-menu {
opacity: 1;
}
.scrollnavbar .v-list .v-list-group__items, .leftPadding {
.hide-menu { margin-left: 0px;
opacity: 1; }
}
.leftPadding {
margin-left: 0px;
}
} }
@media only screen and (min-width: 1170px) { @media only screen and (min-width: 1170px) {
.mini-sidebar { .mini-sidebar {
.logo { .logo {
width: 40px; width: 40px;
overflow: hidden; overflow: hidden;
padding-left: 0; padding-left: 0;
}
.profile-logout {
opacity: 0;
width: 0;
}
.scrollnavbar {
.smallCap {
padding: 3px 12px 12px 12px !important;
}
}
.leftSidebar .v-list--density-default .v-list-subheader {
padding-inline-start: 15px !important;
}
.mini-icon {
display: block;
}
.sidebarchip.hide-menu {
opacity: 0;
}
.mini-text {
display: none;
}
.v-list {
padding: 14px !important;
}
.v-list-group__items {
.iconClass {
position: relative;
left: -2px;
}
}
.leftSidebar:hover {
box-shadow: $box-shadow !important;
.mini-icon {
display: none;
}
.sidebarchip.hide-menu {
opacity: 1;
}
.mini-text {
display: block;
}
.profile-logout {
opacity: 1;
width: auto;
}
.scrollnavbar {
.smallCap {
padding: 3px 12px 12px 0px !important;
}
}
.v-list-group__items {
.iconClass {
position: relative;
left: 0px;
}
}
.v-list--density-default .v-list-subheader {
padding-inline-start: 0px !important;
}
.v-list-group__items {
.v-list-item {
.v-list-item__prepend .dot {
opacity:0;
}
}
}
}
.v-navigation-drawer--expand-on-hover:hover {
.logo {
width: 100%;
}
.v-list .v-list-group__items,
.hide-menu {
opacity: 1;
}
}
.profile-img {
margin-left: 0;
&::before {
left: 12px;
}
}
.menu-toggle {
margin-left: 24px;
}
.v-list-group__items {
.v-list-item {
.v-list-item__prepend .dot {
opacity: 1;
}
}
}
} }
.profile-logout {
opacity: 0;
width: 0;
}
.scrollnavbar {
.smallCap {
padding: 3px 12px 12px 12px !important;
}
}
.leftSidebar .v-list--density-default .v-list-subheader {
padding-inline-start: 15px !important;
}
.mini-icon {
display: block;
}
.sidebarchip.hide-menu {
opacity: 0;
}
.mini-text {
display: none;
}
.v-list {
padding: 14px !important;
}
.v-list-group__items {
.iconClass {
position: relative;
left: -2px;
}
}
.leftSidebar:hover {
box-shadow: $box-shadow !important;
.mini-icon {
display: none;
}
.sidebarchip.hide-menu {
opacity: 1;
}
.mini-text {
display: block;
}
.profile-logout {
opacity: 1;
width: auto;
}
.scrollnavbar {
.smallCap {
padding: 3px 12px 12px 0px !important;
}
}
.v-list-group__items {
.iconClass {
position: relative;
left: 0px;
}
}
.v-list--density-default .v-list-subheader {
padding-inline-start: 0px !important;
}
.v-list-group__items {
.v-list-item {
.v-list-item__prepend .dot {
opacity: 0;
}
}
}
}
.v-navigation-drawer--expand-on-hover:hover {
.logo {
width: 100%;
}
.v-list .v-list-group__items,
.hide-menu {
opacity: 1;
}
}
.profile-img {
margin-left: 0;
&::before {
left: 12px;
}
}
.menu-toggle {
margin-left: 24px;
}
.v-list-group__items {
.v-list-item {
.v-list-item__prepend .dot {
opacity: 1;
}
}
}
}
} }
// scrollbar // scrollbar
.ps__rail-y { .ps__rail-y {
z-index: 9; z-index: 9;
} }
.profile-img { .profile-img {
margin-left: 14px; margin-left: 14px;
&::before { &::before {
-webkit-animation: 2.5s blow 0s linear infinite; -webkit-animation: 2.5s blow 0s linear infinite;
animation: 2.5s blow 0s linear infinite; animation: 2.5s blow 0s linear infinite;
position: absolute; position: absolute;
content: ""; content: "";
width: 50px; width: 50px;
height: 50px; height: 50px;
top: 40px; top: 40px;
border-radius: 50%; border-radius: 50%;
z-index: 0; z-index: 0;
left: 26px; left: 26px;
}
@-webkit-keyframes blow {
0% {
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.1);
opacity: 1;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
} }
@-webkit-keyframes blow { 50% {
0% { box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.1); opacity: 1;
opacity: 1; -webkit-transform: scale3d(1, 1, 0.5);
-webkit-transform: scale3d(1, 1, 0.5); transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
50% {
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1);
opacity: 1;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
100% {
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0.1);
opacity: 0;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
} }
@keyframes blow { 100% {
0% { box-shadow: 0 0 0 20px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.1); opacity: 0;
opacity: 1; -webkit-transform: scale3d(1, 1, 0.5);
-webkit-transform: scale3d(1, 1, 0.5); transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
50% {
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1);
opacity: 1;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
100% {
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0.1);
opacity: 0;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
} }
} }
@keyframes blow {
0% {
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.1);
opacity: 1;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
50% {
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1);
opacity: 1;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
100% {
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0.1);
opacity: 0;
-webkit-transform: scale3d(1, 1, 0.5);
transform: scale3d(1, 1, 0.5);
}
}
}
+183 -183
View File
@@ -1,240 +1,240 @@
// //
// common // common
// //
.inside-left-sidebar { .inside-left-sidebar {
.left-part { .left-part {
width: 240px; width: 240px;
} }
} }
// //
//Full Calendar //Full Calendar
.fc { .fc {
.fc-button-group { .fc-button-group {
>.fc-button { > .fc-button {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 7px 10px; padding: 7px 10px;
border: 0; border: 0;
font-size: 14px; font-size: 14px;
background-color: rgba(var(--v-theme-grey200)); background-color: rgba(var(--v-theme-grey200));
color: rgba(var(--v-theme-textPrimary)); color: rgba(var(--v-theme-textPrimary));
.fc-icon{ .fc-icon {
color: rgba(var(--v-theme-textPrimary)); color: rgba(var(--v-theme-textPrimary));
font-size: 20px; font-size: 20px;
} }
&:hover,&:focus{ &:hover,
background-color: rgba(var(--v-theme-textPrimary)); &:focus {
color: #fff; background-color: rgba(var(--v-theme-textPrimary));
.fc-icon{ color: #fff;
color: #fff; .fc-icon {
} color: #fff;
}
}
.fc-button-primary:not(:disabled):active{
background-color: rgba(var(--v-theme-textPrimary));
}
.fc-button-primary:not(:disabled).fc-button-active {
background-color: rgb(var(--v-theme-textPrimary));
} }
} }
.fc-today-button{ }
padding: 7px 20px; .fc-button-primary:not(:disabled):active {
border: 0; background-color: rgba(var(--v-theme-textPrimary));
background-color: rgba(var(--v-theme-primary)); }
} .fc-button-primary:not(:disabled).fc-button-active {
background-color: rgb(var(--v-theme-textPrimary));
}
}
.fc-today-button {
padding: 7px 20px;
border: 0;
background-color: rgba(var(--v-theme-primary));
}
.fc-prev-button{ .fc-prev-button {
border-radius: 30px 0 0 30px; border-radius: 30px 0 0 30px;
} }
.fc-toolbar-title{ .fc-toolbar-title {
font-weight: 600; font-weight: 600;
} }
.fc-event-title{ .fc-event-title {
font-size: 14px; font-size: 14px;
padding: 3px 6px; padding: 3px 6px;
} }
.fc-button { .fc-button {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
text-transform: capitalize; text-transform: capitalize;
.fc-icon { .fc-icon {
font-size: 1.5em; font-size: 1.5em;
vertical-align: unset; vertical-align: unset;
} }
} }
.fc-daygrid-day-number{ .fc-daygrid-day-number {
color: rgba(var(--v-theme-textSecondary)); color: rgba(var(--v-theme-textSecondary));
font-size: 15px; font-size: 15px;
} }
.fc-button-primary { .fc-button-primary {
background: rgb(var(--v-theme-primary)); background: rgb(var(--v-theme-primary));
border-color: rgb(var(--v-theme-primary)); border-color: rgb(var(--v-theme-primary));
color: #fff; color: #fff;
&:hover { &:hover {
background-color: rgb(var(--v-theme-primary)); background-color: rgb(var(--v-theme-primary));
border-color: rgb(var(--v-theme-primary)); border-color: rgb(var(--v-theme-primary));
} }
&:not(:disabled).fc-button-active { &:not(:disabled).fc-button-active {
background-color: rgb(var(--v-theme-primary)); background-color: rgb(var(--v-theme-primary));
border-color: rgb(var(--v-theme-primary)); border-color: rgb(var(--v-theme-primary));
&:focus { &:focus {
box-shadow: none; box-shadow: none;
} }
} }
&:not(:disabled) { &:not(:disabled) {
&:active { &:active {
background-color: rgb(var(--v-theme-primary)); background-color: rgb(var(--v-theme-primary));
border-color: rgb(var(--v-theme-primary)); border-color: rgb(var(--v-theme-primary));
&:focus { &:focus {
box-shadow: none; box-shadow: none;
} }
} }
} }
&:disabled { &:disabled {
background-color: rgb(var(--v-theme-primary)); background-color: rgb(var(--v-theme-primary));
border-color: rgb(var(--v-theme-primary)); border-color: rgb(var(--v-theme-primary));
opacity: 1; opacity: 1;
} }
} }
.fc-col-header-cell-cushion { .fc-col-header-cell-cushion {
display: inline-block; display: inline-block;
padding: 10px 5px; padding: 10px 5px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
} }
.fc-button-primary:not(:disabled).fc-button-active{ .fc-button-primary:not(:disabled).fc-button-active {
background-color: rgb(var(--v-theme-textPrimary)); background-color: rgb(var(--v-theme-textPrimary));
} }
} }
.fc-theme-standard { .fc-theme-standard {
td { td {
border: 1px solid rgba(var(--v-border-color), 1) !important; border: 1px solid rgba(var(--v-border-color), 1) !important;
} }
th { th {
border: 1px solid rgba(var(--v-border-color), 1) !important; border: 1px solid rgba(var(--v-border-color), 1) !important;
border-bottom: 0 !important; border-bottom: 0 !important;
background-color: rgba(var(--v-theme-grey200)); background-color: rgba(var(--v-theme-grey200));
height: 56px; height: 56px;
vertical-align: middle; vertical-align: middle;
color: rgba(var(--v-theme-textSecondary)); color: rgba(var(--v-theme-textSecondary));
} }
.fc-scrollgrid { .fc-scrollgrid {
border: 0 !important; border: 0 !important;
} }
} }
.fc-h-event { .fc-h-event {
background-color: rgb(var(--v-theme-primary)); background-color: rgb(var(--v-theme-primary));
border: 0; border: 0;
display: block; display: block;
} }
.fc-direction-ltr { .fc-direction-ltr {
.fc-button-group { .fc-button-group {
>.fc-button { > .fc-button {
&:not(:last-child) { &:not(:last-child) {
border-bottom-left-radius: 30px; border-bottom-left-radius: 30px;
border-top-left-radius: 30px; border-top-left-radius: 30px;
} }
&:not(:first-child) { &:not(:first-child) {
border-bottom-right-radius: 30px; border-bottom-right-radius: 30px;
border-top-right-radius: 30px; border-top-right-radius: 30px;
margin-left: -1px; margin-left: -1px;
} }
} }
} }
} }
.fc-button-group { .fc-button-group {
.fc-dayGridMonth-button { .fc-dayGridMonth-button {
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px !important;
border-top-right-radius: 0px !important; border-top-right-radius: 0px !important;
padding: 7px 20px !important; padding: 7px 20px !important;
} }
.fc-timeGridDay-button { .fc-timeGridDay-button {
border-bottom-left-radius: 0px !important; border-bottom-left-radius: 0px !important;
border-top-left-radius: 0px !important; border-top-left-radius: 0px !important;
padding: 7px 20px !important; padding: 7px 20px !important;
} }
.fc-timeGridWeek-button { .fc-timeGridWeek-button {
border-radius: 0 !important; border-radius: 0 !important;
padding: 7px 20px !important; padding: 7px 20px !important;
} }
} }
.fc-today-button { .fc-today-button {
border-radius: 30px !important; border-radius: 30px !important;
font-size: 14px; font-size: 14px;
} }
@media screen and (max-width:600px) { @media screen and (max-width: 600px) {
.fc { .fc {
.fc-toolbar { .fc-toolbar {
display: block; display: block;
text-align: center; text-align: center;
} }
} }
.fc-toolbar-chunk { .fc-toolbar-chunk {
.fc-toolbar-title { .fc-toolbar-title {
margin: 15px 0; margin: 15px 0;
} }
} }
} }
.customTab { .customTab {
.v-btn { .v-btn {
&.v-tab-item--selected { &.v-tab-item--selected {
background-color: rgb(var(--v-theme-lightprimary)) !important; background-color: rgb(var(--v-theme-lightprimary)) !important;
.icon { .icon {
background-color: rgb(var(--v-theme-primary)) !important; background-color: rgb(var(--v-theme-primary)) !important;
color: #fff !important; color: #fff !important;
} }
} }
} }
} }
.email-items { .email-items {
padding: 0px 24px 12px; padding: 0px 24px 12px;
&.selected-email { &.selected-email {
.email-title { .email-title {
color: rgb(var(--v-theme-primary)) !important; color: rgb(var(--v-theme-primary)) !important;
} }
} }
&:hover { &:hover {
background-color: rgb(var(--v-theme-hoverColor)); background-color: rgb(var(--v-theme-hoverColor));
.email-title { .email-title {
color: rgb(var(--v-theme-primary)) !important; color: rgb(var(--v-theme-primary)) !important;
} }
}
}
} }
.email-content { .email-content {
p { p {
margin: 10px 0; margin: 10px 0;
} }
} }
+32 -32
View File
@@ -1,59 +1,59 @@
// Light Theme Colors // Light Theme Colors
.v-theme--AQUA_THEME { .v-theme--AQUA_THEME {
--v-theme-primary: 0, 116, 186; --v-theme-primary: 0, 116, 186;
--v-theme-secondary: 71, 215, 188; --v-theme-secondary: 71, 215, 188;
--v-theme-lightprimary: 229, 241, 248; --v-theme-lightprimary: 229, 241, 248;
--v-theme-lightsecondary: 237, 251, 247; --v-theme-lightsecondary: 237, 251, 247;
} }
.v-theme--PURPLE_THEME { .v-theme--PURPLE_THEME {
--v-theme-primary: 118, 62, 189; --v-theme-primary: 118, 62, 189;
--v-theme-secondary: 148, 208, 214; --v-theme-secondary: 148, 208, 214;
--v-theme-lightprimary: 242, 236, 249; --v-theme-lightprimary: 242, 236, 249;
--v-theme-lightsecondary: 237, 248, 250; --v-theme-lightsecondary: 237, 248, 250;
} }
.v-theme--GREEN_THEME { .v-theme--GREEN_THEME {
--v-theme-primary: 10, 126, 164; --v-theme-primary: 10, 126, 164;
--v-theme-secondary: 204, 218, 78; --v-theme-secondary: 204, 218, 78;
--v-theme-lightprimary: 230, 242, 246; --v-theme-lightprimary: 230, 242, 246;
--v-theme-lightsecondary: 250, 251, 239; --v-theme-lightsecondary: 250, 251, 239;
} }
.v-theme--CYAN_THEME { .v-theme--CYAN_THEME {
--v-theme-primary: 1, 192, 200; --v-theme-primary: 1, 192, 200;
--v-theme-secondary: 251, 150, 120; --v-theme-secondary: 251, 150, 120;
--v-theme-lightprimary: 235, 249, 250; --v-theme-lightprimary: 235, 249, 250;
--v-theme-lightsecondary: 255, 245, 242; --v-theme-lightsecondary: 255, 245, 242;
} }
.v-theme--ORANGE_THEME { .v-theme--ORANGE_THEME {
--v-theme-primary: 250, 137, 107; --v-theme-primary: 250, 137, 107;
--v-theme-secondary: 0, 116, 186; --v-theme-secondary: 0, 116, 186;
--v-theme-lightprimary: 251, 242, 239; --v-theme-lightprimary: 251, 242, 239;
--v-theme-lightsecondary: 239, 249, 255; --v-theme-lightsecondary: 239, 249, 255;
} }
.v-theme--DARK_AQUA_THEME{ .v-theme--DARK_AQUA_THEME {
--v-theme-primary: 0, 116, 186; --v-theme-primary: 0, 116, 186;
--v-theme-secondary: 71, 215, 188; --v-theme-secondary: 71, 215, 188;
} }
.v-theme--DARK_PURPLE_THEME { .v-theme--DARK_PURPLE_THEME {
--v-theme-primary: 118, 62, 189; --v-theme-primary: 118, 62, 189;
--v-theme-secondary: 148, 208, 214; --v-theme-secondary: 148, 208, 214;
} }
.v-theme--DARK_GREEN_THEME { .v-theme--DARK_GREEN_THEME {
--v-theme-primary: 10, 126, 164; --v-theme-primary: 10, 126, 164;
--v-theme-secondary: 204, 218, 78; --v-theme-secondary: 204, 218, 78;
} }
.v-theme--DARK_CYAN_THEME { .v-theme--DARK_CYAN_THEME {
--v-theme-primary: 1, 192, 200; --v-theme-primary: 1, 192, 200;
--v-theme-secondary: 251, 150, 120; --v-theme-secondary: 251, 150, 120;
} }
.v-theme--DARK_ORANGE_THEME { .v-theme--DARK_ORANGE_THEME {
--v-theme-primary: 250, 137, 107; --v-theme-primary: 250, 137, 107;
--v-theme-secondary: 0, 116, 186; --v-theme-secondary: 0, 116, 186;
} }
+3 -3
View File
@@ -189,8 +189,8 @@ const CHIRPY_JEKYLL_DARK_THEME: ThemeTypes = {
"border-opacity": 1, "border-opacity": 1,
}, },
colors: { colors: {
primary: "#8ab4f8", primary: "#07a8f7",
secondary: "#40b883", secondary: "#8ab4f8",
info: "#0075d1", info: "#0075d1",
success: "#0fa30f", success: "#0fa30f",
accent: "#8ab4f8", accent: "#8ab4f8",
@@ -199,7 +199,7 @@ const CHIRPY_JEKYLL_DARK_THEME: ThemeTypes = {
purple: "#b370f5", purple: "#b370f5",
indigo: "#7c6af0", indigo: "#7c6af0",
lightprimary: "#1a2540", lightprimary: "#1a2540",
lightsecondary: "#0d2e20", lightsecondary: "#072a3a",
lightsuccess: "#163c24", lightsuccess: "#163c24",
lighterror: "#3a0000", lighterror: "#3a0000",
lightwarning: "#3a2a10", lightwarning: "#3a2a10",
+1
View File
@@ -4,6 +4,7 @@ import markedKatex from "marked-katex-extension"
import "katex/dist/katex.min.css" import "katex/dist/katex.min.css"
marked.use(markedKatex({ throwOnError: false })) marked.use(markedKatex({ throwOnError: false }))
marked.use({ breaks: true })
export function renderMarkdown(source: string): string { export function renderMarkdown(source: string): string {
return DOMPurify.sanitize(marked.parse(source) as string) return DOMPurify.sanitize(marked.parse(source) as string)