✨ (cards) implements spaced repetition cards
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="flux-note content note-container">
|
<main class="flux-note repo-note content note-container">
|
||||||
<div class="note readme">
|
<div class="note readme">
|
||||||
<header-note v-if="withHeader" class="header" :user="user" :repo="repo" />
|
<header-note v-if="withHeader" class="header" :user="user" :repo="repo" />
|
||||||
<div class="repo-title-breadcrumb">
|
<div class="repo-title-breadcrumb">
|
||||||
@@ -26,7 +26,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!hasContent">No content here 📝</div>
|
<div v-else-if="!hasContent">No content here 📝</div>
|
||||||
<p class="note-display" v-html="renderedContent"></p>
|
<p
|
||||||
|
v-else-if="withContent"
|
||||||
|
class="note-display"
|
||||||
|
v-html="renderedContent"
|
||||||
|
></p>
|
||||||
</div>
|
</div>
|
||||||
<stacked-note
|
<stacked-note
|
||||||
v-for="(stackedNote, index) in stackedNotes"
|
v-for="(stackedNote, index) in stackedNotes"
|
||||||
@@ -74,6 +78,7 @@ export default defineComponent({
|
|||||||
repo: { type: String, required: true },
|
repo: { type: String, required: true },
|
||||||
content: { type: String, required: false, default: null },
|
content: { type: String, required: false, default: null },
|
||||||
parseContent: { type: Boolean, required: false, default: true },
|
parseContent: { type: Boolean, required: false, default: true },
|
||||||
|
withContent: { type: Boolean, required: false, default: true },
|
||||||
withHeader: { type: Boolean, required: false, default: true }
|
withHeader: { type: Boolean, required: false, default: true }
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
@@ -138,12 +143,6 @@ export default defineComponent({
|
|||||||
$header-height: 40px;
|
$header-height: 40px;
|
||||||
|
|
||||||
.flux-note {
|
.flux-note {
|
||||||
font-family: var(--font-family);
|
|
||||||
color: var(--font-color);
|
|
||||||
background-color: var(--background-color);
|
|
||||||
transition-property: color, background-color;
|
|
||||||
transition: cubic-bezier(0.39, 0.575, 0.565, 1) 0.2s;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
>
|
>
|
||||||
<img src="@/assets/icons/dark-left-arrow.svg" alt="go back left arrow" />
|
<img src="@/assets/icons/dark-left-arrow.svg" alt="go back left arrow" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="special-folder"
|
||||||
|
:to="{ name: 'SpacedRepetitionCard', params: { user, repo } }"
|
||||||
|
>
|
||||||
|
cards
|
||||||
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
class="special-folder"
|
class="special-folder"
|
||||||
:to="{ name: 'DraftNotes', params: { user, repo } }"
|
:to="{ name: 'DraftNotes', params: { user, repo } }"
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ export const useFile = (sha: string, retrieveContent = true) => {
|
|||||||
const fromCache = ref(false)
|
const fromCache = ref(false)
|
||||||
|
|
||||||
const content = ref('')
|
const content = ref('')
|
||||||
|
const rawContent = ref('')
|
||||||
|
|
||||||
|
const getRawContent = async () => {
|
||||||
|
const fileContent = await getCachedFileContent()
|
||||||
|
if (!fileContent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.value = render(fileContent)
|
||||||
|
rawContent.value = fileContent
|
||||||
|
return rawContent.value
|
||||||
|
}
|
||||||
|
|
||||||
const getContent = async () => {
|
const getContent = async () => {
|
||||||
const fileContent = await getCachedFileContent()
|
const fileContent = await getCachedFileContent()
|
||||||
@@ -18,6 +29,7 @@ export const useFile = (sha: string, retrieveContent = true) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
content.value = render(fileContent)
|
content.value = render(fileContent)
|
||||||
|
rawContent.value = fileContent
|
||||||
return content.value
|
return content.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +57,7 @@ export const useFile = (sha: string, retrieveContent = true) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
content,
|
||||||
|
getRawContent,
|
||||||
getContent,
|
getContent,
|
||||||
getCachedFileContent,
|
getCachedFileContent,
|
||||||
fromCache
|
fromCache
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const useLinks = (className: string, sha?: string) => {
|
|||||||
|
|
||||||
const linkNote: EventListener = (event) => {
|
const linkNote: EventListener = (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
const href = target.getAttribute('href')
|
const href = target.getAttribute('href')
|
||||||
|
|
||||||
|
|||||||
51
src/modules/card/hooks/useSpacedRepetitionCards.ts
Normal file
51
src/modules/card/hooks/useSpacedRepetitionCards.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useFile } from '@/hooks/useFile.hook'
|
||||||
|
import { useLinks } from '@/hooks/useLinks.hook'
|
||||||
|
import { useMarkdown } from '@/hooks/useMarkdown.hook'
|
||||||
|
import { Card } from '@/modules/card/models/Card'
|
||||||
|
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store'
|
||||||
|
import { asyncComputed } from '@vueuse/core'
|
||||||
|
import { computed, nextTick, watch } from 'vue'
|
||||||
|
|
||||||
|
export const useSpacedRepetitionCards = () => {
|
||||||
|
const { renderString } = useMarkdown()
|
||||||
|
const store = useUserRepoStore()
|
||||||
|
const { listenToClick } = useLinks('flip-card')
|
||||||
|
|
||||||
|
const cardFiles = computed(() =>
|
||||||
|
store.files.filter(
|
||||||
|
(file) => file.path?.startsWith('_cards') && file.path?.endsWith('.md')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const cards = asyncComputed(async () => {
|
||||||
|
const cards: Card[] = []
|
||||||
|
for (const cardFile of cardFiles.value) {
|
||||||
|
if (!cardFile.sha) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { getRawContent } = useFile(cardFile.sha, false)
|
||||||
|
const content = await getRawContent()
|
||||||
|
|
||||||
|
const [front, back, references] =
|
||||||
|
decodeURIComponent(escape(atob(content ?? '')))?.split('___') ?? []
|
||||||
|
|
||||||
|
cards.push({
|
||||||
|
front: renderString(front),
|
||||||
|
back: renderString(back),
|
||||||
|
references: renderString(references)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cards
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
cards,
|
||||||
|
() =>
|
||||||
|
nextTick(() => {
|
||||||
|
listenToClick()
|
||||||
|
}),
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return { cards }
|
||||||
|
}
|
||||||
5
src/modules/card/models/Card.ts
Normal file
5
src/modules/card/models/Card.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface Card {
|
||||||
|
front: string
|
||||||
|
back: string
|
||||||
|
references: string
|
||||||
|
}
|
||||||
@@ -41,6 +41,15 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "draft-notes" */ '@/views/DraftNotes.vue')
|
import(/* webpackChunkName: "draft-notes" */ '@/views/DraftNotes.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/:user/:repo/spaced-repetition',
|
||||||
|
name: 'SpacedRepetitionCard',
|
||||||
|
props: true,
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "spaced-repetition-card" */ '@/views/SpacedRepetitionCard.vue'
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/about',
|
||||||
name: 'About',
|
name: 'About',
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ a {
|
|||||||
background-color: $link;
|
background-color: $link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-note {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
color: var(--font-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
transition-property: color, background-color;
|
||||||
|
transition: cubic-bezier(0.39, 0.575, 0.565, 1) 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|||||||
110
src/views/SpacedRepetitionCard.vue
Normal file
110
src/views/SpacedRepetitionCard.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="spaced-repetition-card repo-note">
|
||||||
|
<flux-note
|
||||||
|
key="spaced-repetition-card"
|
||||||
|
:user="user"
|
||||||
|
:repo="repo"
|
||||||
|
:with-content="false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(card, i) in cards"
|
||||||
|
:key="i"
|
||||||
|
class="flip-card"
|
||||||
|
:class="{ flipped }"
|
||||||
|
@click="flip"
|
||||||
|
>
|
||||||
|
<div class="flip-card-inner">
|
||||||
|
<div
|
||||||
|
class="flip-card-front flip-card-content"
|
||||||
|
v-html="card.front"
|
||||||
|
></div>
|
||||||
|
<div class="flip-card-back flip-card-content">
|
||||||
|
<div class="back" v-html="card.back"></div>
|
||||||
|
<hr />
|
||||||
|
<div class="references" v-html="card.references"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</flux-note>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { useSpacedRepetitionCards } from '@/modules/card/hooks/useSpacedRepetitionCards'
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
import FluxNote from '@/components/FluxNote.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'SpacedRepetitionCard',
|
||||||
|
components: {
|
||||||
|
FluxNote
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
user: { type: String, required: true },
|
||||||
|
repo: { type: String, required: true }
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { cards } = useSpacedRepetitionCards()
|
||||||
|
const flipped = ref(false)
|
||||||
|
|
||||||
|
const flip = () => (flipped.value = !flipped.value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
cards,
|
||||||
|
flipped,
|
||||||
|
flip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.spaced-repetition-card {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
.flip-card {
|
||||||
|
background-color: transparent;
|
||||||
|
width: 400px;
|
||||||
|
height: 200px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card-inner {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: cubic-bezier(0.39, 0.575, 0.565, 1) 0.4s;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card.flipped .flip-card-inner {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card-front,
|
||||||
|
.flip-card-back {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card-content {
|
||||||
|
background-color: #ebebeb;
|
||||||
|
color: var(--font-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card-back {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user