Upload and test
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/Ganbatte.astro
|
||||
|
||||
import styles from "./Ganbatte.module.scss";
|
||||
|
||||
interface Props {
|
||||
toc?: string;
|
||||
tocLevel?: string;
|
||||
imageAlt?: string;
|
||||
}
|
||||
|
||||
const { toc, tocLevel = "1", imageAlt = "MangoPig Ganbatte" } = Astro.props;
|
||||
---
|
||||
|
||||
<blockquote class={styles.ganbatte} data-toc={toc} data-toc-level={tocLevel}>
|
||||
<slot />
|
||||
|
||||
<picture>
|
||||
<img src="https://pic.mangopig.tech/i/4c4d1b5f-b9ce-4952-a1b4-991b19c0adb5.png" alt={imageAlt} />
|
||||
</picture>
|
||||
</blockquote>
|
||||
@@ -0,0 +1,41 @@
|
||||
/* Path: src/components/Post/Blockquotes/Ganbatte.module.scss */
|
||||
|
||||
.ganbatte {
|
||||
background-color: #fff45e1a;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
|
||||
picture {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
|
||||
margin: 0;
|
||||
|
||||
width: 200px;
|
||||
max-width: 30%;
|
||||
|
||||
transform: rotate(10deg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
margin-right: 220px;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 30px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/Important.astro
|
||||
|
||||
import styles from "./Important.module.scss";
|
||||
---
|
||||
|
||||
<blockquote class={styles.important}>
|
||||
<slot />
|
||||
<picture class={styles.sticker}>
|
||||
<img src="https://pic.mangopig.tech/i/7eb5b343-5ddf-47ae-a272-4b82ca3d53d7.webp" alt="MangoPig Important" />
|
||||
</picture>
|
||||
</blockquote>
|
||||
@@ -0,0 +1,55 @@
|
||||
/* Path: src/components/Post/Blockquotes/Important.module.scss */
|
||||
|
||||
.important {
|
||||
background-color: #ff5e5e33;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
.sticker {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: -10px;
|
||||
|
||||
margin: 0;
|
||||
|
||||
width: 100px;
|
||||
max-width: 30%;
|
||||
|
||||
transform: rotate(10deg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
margin-top: 20px;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
span {
|
||||
// Place in middle vertically
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 30px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/Info.astro
|
||||
|
||||
import styles from "./Info.module.scss";
|
||||
---
|
||||
|
||||
<blockquote class={styles.info}>
|
||||
<slot />
|
||||
<picture class={styles.sticker}>
|
||||
<img src="https://pic.mangopig.tech/i/ebf2e26a-8791-4277-90cb-079ad7454aef.webp" alt="MangoPig Ganbattte" />
|
||||
</picture>
|
||||
</blockquote>
|
||||
@@ -0,0 +1,53 @@
|
||||
/* Path: src/components/Post/Blockquotes/Info.module.scss */
|
||||
|
||||
.info {
|
||||
background-color: #5efaff1a;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
|
||||
.sticker {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
|
||||
margin: 0;
|
||||
|
||||
width: 100px;
|
||||
max-width: 30%;
|
||||
|
||||
transform: rotate(10deg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
margin-top: 20px;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
span {
|
||||
// Place in middle vertically
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 30px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/QA.astro
|
||||
import styles from "./QA.module.scss";
|
||||
---
|
||||
|
||||
<div class={styles.qaContainer}>
|
||||
{/* The Question Section */}
|
||||
<div class={styles.questionHeader}>
|
||||
<span class={styles.prefix}>Q:</span>
|
||||
<span class={styles.questionText}>
|
||||
<slot name="question" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* The Answer Section (Default Slot) */}
|
||||
<div class={styles.answerBody}>
|
||||
<span class={styles.prefix}>A:</span>
|
||||
<div class={styles.answerContent}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* The Sticker */}
|
||||
<picture class={styles.sticker}>
|
||||
<img src="https://pic.mangopig.tech/i/4c4d1b5f-b9ce-4952-a1b4-991b19c0adb5.png" alt="Thinking MangoPig" />
|
||||
</picture>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
/* Path: src/components/Post/Blockquotes/QA.module.scss */
|
||||
|
||||
.qaContainer {
|
||||
// Use a yellowish tint to differentiate from Info block
|
||||
background-color: #fff45e26;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
// --- Sticker Logic (Same as Info block) ---
|
||||
.sticker {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: 0;
|
||||
width: 100px;
|
||||
max-width: 30%;
|
||||
// Rotate opposite way for variety
|
||||
transform: rotate(-10deg);
|
||||
pointer-events: none;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
//Common prefix style (Q: and A:)
|
||||
.prefix {
|
||||
font-weight: 800;
|
||||
color: #ffbd72; // Matches your H2 color scheme
|
||||
margin-right: 12px;
|
||||
display: inline-block;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
// --- Question Section ---
|
||||
.questionHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
// Ensure text doesn't hit the sticker
|
||||
margin-right: 90px;
|
||||
color: color-adjust(primary, 0, 0);
|
||||
}
|
||||
|
||||
// --- Answer Section ---
|
||||
.answerBody {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
// Ensure text doesn't hit the sticker
|
||||
margin-right: 90px;
|
||||
}
|
||||
|
||||
.answerContent {
|
||||
flex: 1;
|
||||
line-height: 1.6;
|
||||
|
||||
// Handle standard markdown elements inside the answer slot
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 1em;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
// Path: src/components/Post/FloatingTOC.astro
|
||||
import styles from "./FloatingTOC.module.scss";
|
||||
---
|
||||
|
||||
<nav class={styles.toc} aria-label="Table of Contents">
|
||||
<ul id="toc-list"></ul>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function generateTOC() {
|
||||
const content = document.getElementById("lesson-container");
|
||||
const list = document.getElementById("toc-list");
|
||||
|
||||
if (list) list.innerHTML = "";
|
||||
if (!content || !list) return;
|
||||
|
||||
const targets = content.querySelectorAll("[data-toc]");
|
||||
|
||||
targets.forEach((el, index) => {
|
||||
const label = el.getAttribute("data-toc") || "";
|
||||
const level = el.getAttribute("data-toc-level") || "1";
|
||||
|
||||
// --- CHANGED SECTION START: ID Generation (Slugify) ---
|
||||
if (!el.id) {
|
||||
// Convert "Setting Up Developer Environment" -> "setting-up-developer-environment"
|
||||
const slug = label
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, "") // Remove non-word chars
|
||||
.replace(/[\s_-]+/g, "-") // Replace spaces with dashes
|
||||
.replace(/^-+|-+$/g, ""); // Trim dashes from start/end
|
||||
|
||||
// Safety check: If ID exists or slug is empty, fallback to index
|
||||
if (!slug || document.getElementById(slug)) {
|
||||
el.id = `section-${index}`;
|
||||
} else {
|
||||
el.id = slug;
|
||||
}
|
||||
}
|
||||
// --- CHANGED SECTION END ---
|
||||
|
||||
const li = document.createElement("li");
|
||||
const a = document.createElement("a");
|
||||
|
||||
li.setAttribute("data-level", level);
|
||||
|
||||
a.href = `#${el.id}`;
|
||||
a.innerHTML = `<span class="toc-text">${label}</span>`;
|
||||
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
// Optional: Update URL hash without jumping
|
||||
history.pushState(null, "", `#${el.id}`);
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
document.querySelectorAll("#toc-list a").forEach((link) => link.classList.remove("active"));
|
||||
a.classList.add("active");
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-50% 0px -50% 0px", threshold: 0 }
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
generateTOC();
|
||||
document.addEventListener("astro:after-swap", generateTOC);
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
/* Path: src/components/Post/FloatingTOC.module.scss */
|
||||
|
||||
.toc {
|
||||
// 1. Container Layout
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 100;
|
||||
|
||||
// 2. THE FIX: Make container wide enough to hold the text
|
||||
width: 300px;
|
||||
|
||||
// 3. THE FIX: Allow clicks to pass through the empty area
|
||||
pointer-events: none;
|
||||
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
|
||||
// Keep scrolling functionality
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Nesting protections
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end; // Keep lines on the right
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%; // Ensure list item spans width
|
||||
}
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
text-decoration: none;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
|
||||
// 4. THE FIX: Re-enable clicks on the actual links
|
||||
pointer-events: auto;
|
||||
|
||||
// --- THE LINE ---
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 3px;
|
||||
background-color: #ffb8b8;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// --- THE TEXT ---
|
||||
:global(.toc-text) {
|
||||
position: absolute;
|
||||
|
||||
// 5. THE FIX: Position relative to the line inside the new wider box
|
||||
// Since the box is 300px wide, we don't need 'right: 150%'.
|
||||
// We just place it to the left of the line.
|
||||
right: 60px;
|
||||
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: color-adjust(text, 0, 0);
|
||||
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: none;
|
||||
|
||||
background: color-adjust(bg, 0, 0.8);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// --- HOVER STATE ---
|
||||
&:hover {
|
||||
:global(.toc-text) {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
&::after {
|
||||
background-color: color-adjust(text, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// --- ACTIVE STATE ---
|
||||
&:global(.active) {
|
||||
&::after {
|
||||
background-color: color-adjust(secondary, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- STAGGERED LENGTHS ---
|
||||
li[data-level="1"] {
|
||||
a::after {
|
||||
width: 40px;
|
||||
}
|
||||
a:global(.active)::after {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-level="2"] {
|
||||
a::after {
|
||||
width: 25px;
|
||||
}
|
||||
a:global(.active)::after {
|
||||
width: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-level="3"] {
|
||||
a::after {
|
||||
width: 15px;
|
||||
}
|
||||
a:global(.active)::after {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* Path: src/components/Post/Spoiler.module.scss */
|
||||
|
||||
.spoiler {
|
||||
border-left: 4px solid color-adjust(secondary, 0, 0);
|
||||
padding: 1rem;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: color-adjust(primary, 0, 0);
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spoilerContent {
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
28
00-Lesson-Site/frontend/src/components/Post/Spoiler.tsx
Normal file
28
00-Lesson-Site/frontend/src/components/Post/Spoiler.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Path: src/components/Post/Spoiler.tsx
|
||||
import type { Component, JSX } from "solid-js";
|
||||
import { createSignal } from "solid-js";
|
||||
import styles from "./Spoiler.module.scss";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
buttonText?: string;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
const Spoiler: Component<Props> = (props) => {
|
||||
const [visible, setVisible] = createSignal(false);
|
||||
|
||||
return (
|
||||
<blockquote class={styles.spoiler}>
|
||||
{props.title ? <strong class={styles.spoilerTitle}>{props.title}</strong> : <></>}
|
||||
<button onClick={() => setVisible(!visible())}>
|
||||
{visible() ? "Hide" : "Show"}
|
||||
{props.buttonText ? `${props.buttonText}` : " Answer"}
|
||||
</button>
|
||||
|
||||
{visible() && <div class={styles.spoilerContent}>{props.children}</div>}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spoiler;
|
||||
Reference in New Issue
Block a user