Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/src/html/pages/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@
</button>
</div>

<div class="spacer quoteTagSpacer"></div>

<div class="quoteTagFilter hidden">
<button
class="textButton quoteTagFilterTrigger"
id="quoteTagFilterTrigger"
aria-haspopup="true"
aria-expanded="false"
aria-controls="quoteTagFilterModal"
title="Filter by tag"
>
<i class="fas fa-fw fa-tags"></i>
<span class="quoteTagFilterLabel">Select tags</span>
</button>
</div>

<div class="zen">
<div
class="textButton"
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/html/popups.html
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,31 @@
<button class="saveButton">save</button>
</div>
</dialog>

<!-- Tag modal -->
<dialog id="quoteTagFilterModal" class="modalWrapper hidden">
<div class="modal">
<div class="quoteTagFilterDropdownHeader">
<span class="quoteTagFilterDropdownTitle">Filter by tag</span>
<button
type="button"
class="textButton quoteTagFilterClearBtn"
id="quoteTagFilterModalClearBtn"
title="Clear all tag filters"
>
clear
</button>
</div>

<div
class="quoteTagFilterPills"
id="quoteTagFilterModalPills"
role="group"
aria-label="Quote tags"
></div>
</div>
</dialog>

<dialog id="editPresetModal" class="modalWrapper hidden">
<form class="modal">
<div class="title popupTitle"></div>
Expand Down
157 changes: 157 additions & 0 deletions frontend/src/styles/test.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1662,3 +1662,160 @@ body.fb-arrows {
}
}
}

// Quote Tag Filter
#testConfig {
// Register .quoteTagFilter alongside the other config sections so it gets
// the same grid-auto-flow and textButton padding treatment.
.quoteTagFilter {
display: grid;
grid-auto-flow: column;
justify-content: end;
position: relative; // anchor for the absolute dropdown

.textButton {
padding: var(--verticalPadding) var(--horizontalPadding);

&:first-child {
margin-left: var(--horizontalPadding);
}
&:last-child {
margin-right: var(--horizontalPadding);
}
}
}
}

// Trigger button extras
.quoteTagFilterTrigger {
display: flex;
align-items: center;
gap: 0.35em;
white-space: nowrap;
}

.quoteTagFilterChevron {
font-size: 0.7em;
opacity: 0.5;
transition:
transform 0.2s ease,
opacity 0.2s ease;
}

.quoteTagFilterTrigger[aria-expanded="true"] .quoteTagFilterChevron {
transform: rotate(180deg);
opacity: 1;
}

// Dropdown panel
.quoteTagFilterDropdown {
position: absolute;
// Sit just below the config row; z-index keeps it above #words
top: calc(100% + 6px);
left: 50%;
transform: translateX(-50%) translateY(-4px);
z-index: 200;

min-width: 240px;
padding: 0.6rem;
border-radius: var(--roundness);

background-color: var(--sub-alt-color);
border: 1px solid color-mix(in srgb, var(--sub-color) 40%, transparent);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);

// Hidden by default β€” .open class toggled by JS
opacity: 0;
pointer-events: none;
transition:
opacity 0.15s ease,
transform 0.15s ease;

&.open {
opacity: 1;
pointer-events: all;
transform: translateX(-50%) translateY(0);
}
}

// Dropdown header
.quoteTagFilterDropdownHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid color-mix(in srgb, var(--sub-color) 30%, transparent);
}

.quoteTagFilterDropdownTitle {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--sub-color);
}

// Clear button β€” compact variant of .textButton
.quoteTagFilterClearBtn {
font-size: 0.7rem;
padding: 0.15em 0.5em;
color: var(--sub-color);
visibility: hidden; // shown by JS only when tags are selected
transition: color 0.125s;

&:hover {
color: var(--text-color);
}
}

// Tag pills
.quoteTagFilterPills {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}

// Each pill reuses .textButton as a base class (applied in JS).
// Additional pill-specific overrides live here.
.quoteTagPill {
display: inline-flex;
align-items: center;
gap: 0.3em;
padding: 0.25em 0.65em;
border-radius: 999px; // pill shape
font-size: 0.75rem;
opacity: 0.5;
transition:
background-color 0.125s,
color 0.125s,
opacity 0.125s;

i {
font-size: 0.7em;
}

&:hover {
opacity: 0.85;
}

// Active state mirrors MonkeyType's selected config button pattern
&.active {
opacity: 1;
color: var(--main-color);
background-color: color-mix(in srgb, var(--main-color) 12%, transparent);
}

&.disabled {
opacity: 0.2;
cursor: not-allowed;
pointer-events: none;
filter: grayscale(1);
}
}

// Responsive
@media (max-width: 480px) {
.quoteTagFilterDropdown {
min-width: 190px;
}
}
8 changes: 8 additions & 0 deletions frontend/src/ts/commandline/commandline-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = {
},
},
},
quoteTags: {
subgroup: {
options: "fromSchema",
afterExec: () => {
TestLogic.restart();
},
},
},
//behavior
difficulty: {
subgroup: {
Expand Down
24 changes: 19 additions & 5 deletions frontend/src/ts/config/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as ConfigSchemas from "@monkeytype/schemas/configs";
import { roundTo1 } from "@monkeytype/util/numbers";
import { capitalizeFirstLetter } from "../utils/strings";
import { getDefaultConfig } from "../constants/default-config";
import { areUnsortedArraysEqual } from "../utils/arrays";
// type SetBlock = {
// [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K][];
// };
Expand Down Expand Up @@ -170,13 +171,26 @@ export const configMetadata: ConfigMetadataObject = {
displayString: "quote length",
changeRequiresRestart: true,
group: "test",
overrideConfig: ({ currentConfig }) => {
overrideConfig: ({ value, currentConfig }) => {
const overrides: Partial<ConfigSchemas.Config> = {};
if (
!areUnsortedArraysEqual(value as number[], currentConfig.quoteLength)
) {
overrides.quoteTags = [];
}
if (currentConfig.mode !== "quote") {
return {
mode: "quote",
};
overrides.mode = "quote";
}
return {};
return overrides;
},
},
quoteTags: {
icon: "fa-tags",
displayString: "quote tags",
changeRequiresRestart: false,
group: "test",
overrideValue: ({ value }) => {
return [...new Set(value)];
},
},
language: {
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/ts/config/setters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ import { typedKeys, triggerResize, escapeHTML } from "../utils/misc";
import { camelCaseToWords, capitalizeFirstLetter } from "../utils/strings";
import { Config, setConfigStore } from "./store";
import { FunboxName } from "@monkeytype/schemas/configs";
import { type QuoteTag } from "@monkeytype/schemas/quotes";

export function setQuoteTags(tags: QuoteTag[], nosave?: boolean): boolean {
return setConfig("quoteTags", tags, {
nosave,
});
}

export function toggleQuoteTag(tag: QuoteTag, nosave?: boolean): void {
const current = [...Config.quoteTags];
const idx = current.indexOf(tag);

if (idx === -1) {
current.push(tag);
} else {
current.splice(idx, 1);
}

setQuoteTags(current, nosave);
}

export function setConfig<T extends keyof ConfigSchemas.Config>(
key: T,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/ts/constants/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const obj: Config = {
time: 30,
mode: "time",
quoteLength: [1],
quoteTags: [],
language: "english",
fontSize: 2,
freedomMode: false,
Expand Down
40 changes: 35 additions & 5 deletions frontend/src/ts/controllers/quotes-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import * as DB from "../db";
import Ape from "../ape";
import { tryCatch } from "@monkeytype/util/trycatch";
import { Language } from "@monkeytype/schemas/languages";
import { QuoteData, QuoteDataQuote } from "@monkeytype/schemas/quotes";
import {
QuoteData,
QuoteDataQuote,
QuoteTag,
} from "@monkeytype/schemas/quotes";
import { RequiredProperties } from "../utils/misc";

export type Quote = QuoteDataQuote & {
Expand Down Expand Up @@ -79,6 +83,7 @@ class QuotesController {
length: quote.length,
id: quote.id,
language: data.language,
tags: quote.tags,
group: 0,
};

Expand Down Expand Up @@ -107,6 +112,16 @@ class QuotesController {
return this.quoteCollection;
}

getAvailableTags(quoteGroups: number[]): Set<QuoteTag> {
const tags = new Set<QuoteTag>();
quoteGroups.forEach((group) => {
this.quoteCollection.groups[group]?.forEach((quote) => {
quote.tags?.forEach((tag) => tags.add(tag));
});
});
return tags;
}

getQuoteById(id: number): Quote | undefined {
const targetQuote = this.quoteCollection.quotes.find((quote: Quote) => {
return quote.id === id;
Expand Down Expand Up @@ -148,7 +163,10 @@ class QuotesController {
return randomQuote;
}

getRandomFavoriteQuote(language: Language): Quote | null {
getRandomFavoriteQuote(
language: Language,
tagFilter?: string[],
): Quote | null {
const snapshot = DB.getSnapshot();
if (!snapshot) {
return null;
Expand All @@ -174,10 +192,22 @@ class QuotesController {
return null;
}

const randomQuoteId = randomElementFromArray(quoteIds);
const randomQuote = this.getQuoteById(parseInt(randomQuoteId, 10));
const selectedTags =
(tagFilter?.length ?? 0) > 0 ? new Set(tagFilter) : null;

const matchingQuotes =
selectedTags === null
? quoteIds
.map((quoteId) => this.getQuoteById(parseInt(quoteId, 10)))
.filter((q): q is Quote => q !== undefined)
: quoteIds
.map((quoteId) => this.getQuoteById(parseInt(quoteId, 10)))
.filter((q): q is Quote => q !== undefined)
.filter((q) => (q.tags ?? []).some((t) => selectedTags.has(t)));

if (matchingQuotes.length === 0) return null;

return randomQuote ?? null;
return randomElementFromArray(matchingQuotes) ?? null;
}

isQuoteFavorite({ language: quoteLanguage, id }: Quote): boolean {
Expand Down
Loading
Loading