Radio show website: Full Astro build with 194 episodes imported
Complete website for The Computer Guru Show (radio.azcomputerguru.com): - Astro 6.0.4 static site with React islands - 194 episodes imported from gurushow.com RSS feed - Dark/light mode HSL design system - Persistent audio player with session persistence - Episode archive with search and season filtering - Home page with animated hero, stats, latest episodes - All pages: About, Subscribe, Community, Live, Contact, Blog, 404 - Podcast RSS feed with iTunes namespace - Session log updated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
24
projects/radio-show/website/.gitignore
vendored
Normal file
24
projects/radio-show/website/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
43
projects/radio-show/website/README.md
Normal file
43
projects/radio-show/website/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
14
projects/radio-show/website/astro.config.mjs
Normal file
14
projects/radio-show/website/astro.config.mjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import react from '@astrojs/react';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://radio.azcomputerguru.com',
|
||||||
|
output: 'static',
|
||||||
|
integrations: [react(), mdx(), sitemap()],
|
||||||
|
image: {
|
||||||
|
service: { entrypoint: 'astro/assets/services/sharp' },
|
||||||
|
},
|
||||||
|
});
|
||||||
7135
projects/radio-show/website/package-lock.json
generated
Normal file
7135
projects/radio-show/website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
projects/radio-show/website/package.json
Normal file
29
projects/radio-show/website/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.7",
|
||||||
|
"@astrojs/mdx": "^5.0.0",
|
||||||
|
"@astrojs/react": "^5.0.0",
|
||||||
|
"@astrojs/rss": "^4.0.17",
|
||||||
|
"@astrojs/sitemap": "^3.7.1",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"astro": "^6.0.4",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"wavesurfer.js": "^7.12.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
projects/radio-show/website/public/favicon.ico
Normal file
BIN
projects/radio-show/website/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 655 B |
9
projects/radio-show/website/public/favicon.svg
Normal file
9
projects/radio-show/website/public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
570
projects/radio-show/website/scripts/import-episodes.mjs
Normal file
570
projects/radio-show/website/scripts/import-episodes.mjs
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
/**
|
||||||
|
* import-episodes.mjs
|
||||||
|
*
|
||||||
|
* Fetches all episodes from the Computer Guru Show podcast RSS feed
|
||||||
|
* and converts them into Astro content collection markdown files.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/import-episodes.mjs
|
||||||
|
*
|
||||||
|
* Dependencies: fast-xml-parser, turndown
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { XMLParser } from "fast-xml-parser";
|
||||||
|
import TurndownService from "turndown";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Configuration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const FEED_URL = "https://gurushow.com/feed/podcast";
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const PROJECT_ROOT = join(__dirname, "..");
|
||||||
|
const EPISODES_DIR = join(PROJECT_ROOT, "src", "content", "episodes");
|
||||||
|
|
||||||
|
/** Common tech keywords mapped to tag slugs. */
|
||||||
|
const TAG_KEYWORDS = new Map([
|
||||||
|
["apple", "apple"],
|
||||||
|
["iphone", "iphone"],
|
||||||
|
["ipad", "ipad"],
|
||||||
|
["\\bmac\\b", "mac"],
|
||||||
|
["macbook", "macbook"],
|
||||||
|
["google", "google"],
|
||||||
|
["android", "android"],
|
||||||
|
["chrome", "chrome"],
|
||||||
|
["chromebook", "chromebook"],
|
||||||
|
["microsoft", "microsoft"],
|
||||||
|
["windows", "windows"],
|
||||||
|
["security", "security"],
|
||||||
|
["privacy", "privacy"],
|
||||||
|
["hacking", "security"],
|
||||||
|
["hack", "security"],
|
||||||
|
["malware", "malware"],
|
||||||
|
["ransomware", "ransomware"],
|
||||||
|
["virus", "malware"],
|
||||||
|
["\\bai\\b", "ai"],
|
||||||
|
["artificial.intelligence", "ai"],
|
||||||
|
["chatgpt", "ai"],
|
||||||
|
["openai", "ai"],
|
||||||
|
["machine.learning", "ai"],
|
||||||
|
["space", "space"],
|
||||||
|
["spacex", "space"],
|
||||||
|
["nasa", "space"],
|
||||||
|
["bitcoin", "cryptocurrency"],
|
||||||
|
["crypto", "cryptocurrency"],
|
||||||
|
["cryptocurrency", "cryptocurrency"],
|
||||||
|
["blockchain", "cryptocurrency"],
|
||||||
|
["net.neutrality", "net-neutrality"],
|
||||||
|
["gaming", "gaming"],
|
||||||
|
["playstation", "gaming"],
|
||||||
|
["xbox", "gaming"],
|
||||||
|
["nintendo", "gaming"],
|
||||||
|
["social.media", "social-media"],
|
||||||
|
["facebook", "facebook"],
|
||||||
|
["twitter", "twitter"],
|
||||||
|
["instagram", "instagram"],
|
||||||
|
["tiktok", "social-media"],
|
||||||
|
["amazon", "amazon"],
|
||||||
|
["alexa", "amazon"],
|
||||||
|
["tesla", "tesla"],
|
||||||
|
["samsung", "samsung"],
|
||||||
|
["cybersecurity", "security"],
|
||||||
|
["phishing", "security"],
|
||||||
|
["data.breach", "security"],
|
||||||
|
["linux", "linux"],
|
||||||
|
["ubuntu", "linux"],
|
||||||
|
["roku", "streaming"],
|
||||||
|
["netflix", "streaming"],
|
||||||
|
["streaming", "streaming"],
|
||||||
|
["5g", "5g"],
|
||||||
|
["\\bvr\\b", "vr"],
|
||||||
|
["virtual.reality", "vr"],
|
||||||
|
["augmented.reality", "ar"],
|
||||||
|
["drone", "drones"],
|
||||||
|
["robot", "robotics"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode common HTML entities that appear in RSS feed titles/descriptions.
|
||||||
|
* Handles numeric (–), hex (’), and named (&) entities.
|
||||||
|
*/
|
||||||
|
function decodeHtmlEntities(text) {
|
||||||
|
if (!text) return "";
|
||||||
|
|
||||||
|
const namedEntities = {
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
""": '"',
|
||||||
|
"'": "'",
|
||||||
|
" ": " ",
|
||||||
|
"–": "\u2013",
|
||||||
|
"—": "\u2014",
|
||||||
|
"‘": "\u2018",
|
||||||
|
"’": "\u2019",
|
||||||
|
"“": "\u201C",
|
||||||
|
"”": "\u201D",
|
||||||
|
"…": "\u2026",
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
// Named entities
|
||||||
|
for (const [entity, char] of Object.entries(namedEntities)) {
|
||||||
|
result = result.replaceAll(entity, char);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric decimal entities: –
|
||||||
|
result = result.replace(/&#(\d+);/g, (_match, dec) =>
|
||||||
|
String.fromCodePoint(Number(dec))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Numeric hex entities: ’
|
||||||
|
result = result.replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) =>
|
||||||
|
String.fromCodePoint(parseInt(hex, 16))
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse season and episode numbers from a title string.
|
||||||
|
* Looks for patterns like "S10E21", "S1E3", "s02e05", case-insensitive.
|
||||||
|
* Returns { season, episode } or null if not found.
|
||||||
|
*/
|
||||||
|
function parseSeasonEpisode(title) {
|
||||||
|
// Pattern: S{n}E{n} anywhere in the title (with optional space between S and E parts)
|
||||||
|
const match = title.match(/S(\d+)\s*E(\d+)/i);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
season: parseInt(match[1], 10),
|
||||||
|
episode: parseInt(match[2], 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: "Season X Episode Y" pattern
|
||||||
|
const longMatch = title.match(/Season\s+(\d+)\s+Episode\s+(\d+)/i);
|
||||||
|
if (longMatch) {
|
||||||
|
return {
|
||||||
|
season: parseInt(longMatch[1], 10),
|
||||||
|
episode: parseInt(longMatch[2], 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip "Podcast " prefix from episode titles.
|
||||||
|
*/
|
||||||
|
function cleanTitle(rawTitle) {
|
||||||
|
let title = decodeHtmlEntities(rawTitle).trim();
|
||||||
|
// Remove leading "Podcast " (case-insensitive)
|
||||||
|
title = title.replace(/^Podcast\s+/i, "");
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a URL-safe slug from a title string.
|
||||||
|
* - Lowercase
|
||||||
|
* - Replace non-alphanumeric chars with hyphens
|
||||||
|
* - Collapse multiple hyphens
|
||||||
|
* - Trim hyphens from ends
|
||||||
|
* - Max 60 characters
|
||||||
|
*/
|
||||||
|
function slugify(title) {
|
||||||
|
// Remove the S{n}E{n} prefix before slugifying to keep the slug about content
|
||||||
|
const withoutCode = title.replace(/^S\d+\s*E\d+\s*[-\u2013:]?\s*/i, "").trim();
|
||||||
|
const base = withoutCode || title;
|
||||||
|
|
||||||
|
let slug = base
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "");
|
||||||
|
|
||||||
|
if (slug.length > 60) {
|
||||||
|
slug = slug.substring(0, 60).replace(/-$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return slug || "untitled";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a filename for an episode.
|
||||||
|
* Format: s{SS}e{EE}-{slug}.md
|
||||||
|
*/
|
||||||
|
function episodeFilename(season, episode, title) {
|
||||||
|
const s = String(season).padStart(2, "0");
|
||||||
|
const e = String(episode).padStart(2, "0");
|
||||||
|
const slug = slugify(title);
|
||||||
|
return `s${s}e${e}-${slug}.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract tags from a title string based on keyword matching.
|
||||||
|
* Returns a deduplicated, sorted array of tag slugs.
|
||||||
|
*/
|
||||||
|
function extractTags(title, description) {
|
||||||
|
const text = `${title} ${description}`.toLowerCase();
|
||||||
|
const tags = new Set();
|
||||||
|
|
||||||
|
for (const [pattern, tag] of TAG_KEYWORDS) {
|
||||||
|
const regex = new RegExp(pattern, "i");
|
||||||
|
if (regex.test(text)) {
|
||||||
|
tags.add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...tags].sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an ISO date string or RSS pubDate into YYYY-MM-DD format.
|
||||||
|
*/
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return "1970-01-01";
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
if (isNaN(d.getTime())) return "1970-01-01";
|
||||||
|
return d.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize audio URL to HTTPS.
|
||||||
|
*/
|
||||||
|
function normalizeAudioUrl(url) {
|
||||||
|
if (!url) return "";
|
||||||
|
return url.replace(/^http:\/\//i, "https://");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape YAML string values. Wraps in double quotes and escapes inner quotes.
|
||||||
|
*/
|
||||||
|
function yamlString(value) {
|
||||||
|
if (value === undefined || value === null) return '""';
|
||||||
|
const str = String(value);
|
||||||
|
// Escape backslashes first, then double quotes
|
||||||
|
const escaped = str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||||
|
return `"${escaped}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Turndown (HTML -> Markdown) setup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function createTurndown() {
|
||||||
|
const td = new TurndownService({
|
||||||
|
headingStyle: "atx",
|
||||||
|
bulletListMarker: "-",
|
||||||
|
codeBlockStyle: "fenced",
|
||||||
|
emDelimiter: "*",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove script/style elements
|
||||||
|
td.remove(["script", "style"]);
|
||||||
|
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HTML content to clean markdown.
|
||||||
|
* Handles null/empty input gracefully.
|
||||||
|
*/
|
||||||
|
function htmlToMarkdown(html, turndown) {
|
||||||
|
if (!html || typeof html !== "string" || html.trim().length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let md = turndown.turndown(html);
|
||||||
|
|
||||||
|
// Clean up excessive blank lines
|
||||||
|
md = md.replace(/\n{3,}/g, "\n\n");
|
||||||
|
|
||||||
|
// Decode any leftover HTML entities
|
||||||
|
md = decodeHtmlEntities(md);
|
||||||
|
|
||||||
|
return md.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Feed fetching and parsing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function fetchFeed(url) {
|
||||||
|
console.log(`Fetching RSS feed from ${url} ...`);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "GuruShowImporter/1.0",
|
||||||
|
Accept: "application/rss+xml, application/xml, text/xml",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch feed: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const xml = await response.text();
|
||||||
|
console.log(`Received ${xml.length.toLocaleString()} bytes of XML.`);
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFeed(xml) {
|
||||||
|
const parser = new XMLParser({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
attributeNamePrefix: "@_",
|
||||||
|
// Parse CDATA sections
|
||||||
|
cdataPropName: "__cdata",
|
||||||
|
// Keep text nodes
|
||||||
|
textNodeName: "#text",
|
||||||
|
// Do not trim whitespace from values
|
||||||
|
trimValues: false,
|
||||||
|
// Ensure items are always arrays
|
||||||
|
isArray: (name) => name === "item",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parser.parse(xml);
|
||||||
|
const channel = result?.rss?.channel;
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
throw new Error("Invalid RSS feed: no channel element found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = channel.item;
|
||||||
|
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||||
|
throw new Error("No episodes found in feed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Parsed ${items.length} episode items from feed.`);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Episode extraction
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a text value from a parsed XML node, handling CDATA wrappers.
|
||||||
|
*/
|
||||||
|
function extractText(node) {
|
||||||
|
if (node === undefined || node === null) return "";
|
||||||
|
if (typeof node === "string") return node.trim();
|
||||||
|
if (typeof node === "number") return String(node);
|
||||||
|
if (typeof node === "object") {
|
||||||
|
// CDATA wrapped
|
||||||
|
if (node.__cdata !== undefined) {
|
||||||
|
return typeof node.__cdata === "string" ? node.__cdata.trim() : "";
|
||||||
|
}
|
||||||
|
if (node["#text"] !== undefined) {
|
||||||
|
return String(node["#text"]).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single RSS item into an episode data object.
|
||||||
|
*/
|
||||||
|
function processItem(item, index, warnings) {
|
||||||
|
const rawTitle = extractText(item.title);
|
||||||
|
const title = cleanTitle(rawTitle);
|
||||||
|
|
||||||
|
// Parse season/episode
|
||||||
|
const parsed = parseSeasonEpisode(rawTitle);
|
||||||
|
let season, episode;
|
||||||
|
|
||||||
|
if (parsed) {
|
||||||
|
season = parsed.season;
|
||||||
|
episode = parsed.episode;
|
||||||
|
} else {
|
||||||
|
season = 0;
|
||||||
|
episode = index + 1;
|
||||||
|
warnings.push(
|
||||||
|
`[WARNING] Could not parse S/E from title: "${title}" -- using season=0, episode=${episode}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
const pubDate = formatDate(extractText(item.pubDate));
|
||||||
|
|
||||||
|
// Description: prefer content:encoded, fall back to description
|
||||||
|
const contentEncoded = extractText(item["content:encoded"]);
|
||||||
|
const description = extractText(item.description);
|
||||||
|
const bodyHtml = contentEncoded || description || "";
|
||||||
|
|
||||||
|
if (!bodyHtml || bodyHtml.trim().length === 0) {
|
||||||
|
warnings.push(`[WARNING] Empty description for: "${title}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio enclosure
|
||||||
|
let audioUrl = "";
|
||||||
|
let audioSize = 0;
|
||||||
|
const enclosure = item.enclosure;
|
||||||
|
if (enclosure) {
|
||||||
|
audioUrl = normalizeAudioUrl(
|
||||||
|
enclosure["@_url"] || extractText(enclosure.url) || ""
|
||||||
|
);
|
||||||
|
const rawSize =
|
||||||
|
enclosure["@_length"] || extractText(enclosure.length) || "0";
|
||||||
|
audioSize = parseInt(rawSize, 10) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
const duration = extractText(item["itunes:duration"]) || "";
|
||||||
|
|
||||||
|
// Original URL
|
||||||
|
const link = extractText(item.link) || "";
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
const tags = extractTags(title, description);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
pubDate,
|
||||||
|
bodyHtml,
|
||||||
|
audioUrl,
|
||||||
|
audioSize,
|
||||||
|
duration,
|
||||||
|
link,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Markdown file generation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function generateMarkdown(ep, turndown) {
|
||||||
|
const body = htmlToMarkdown(ep.bodyHtml, turndown);
|
||||||
|
|
||||||
|
const tagList =
|
||||||
|
ep.tags.length > 0 ? `[${ep.tags.map((t) => `"${t}"`).join(", ")}]` : "[]";
|
||||||
|
|
||||||
|
const frontmatter = [
|
||||||
|
"---",
|
||||||
|
`title: ${yamlString(ep.title)}`,
|
||||||
|
`season: ${ep.season}`,
|
||||||
|
`episode: ${ep.episode}`,
|
||||||
|
`pubDate: ${ep.pubDate}`,
|
||||||
|
`duration: ${yamlString(ep.duration)}`,
|
||||||
|
`audioUrl: ${yamlString(ep.audioUrl)}`,
|
||||||
|
`audioSize: ${ep.audioSize}`,
|
||||||
|
`episodeType: "full"`,
|
||||||
|
`originalUrl: ${yamlString(ep.link)}`,
|
||||||
|
`featured: false`,
|
||||||
|
`classic: false`,
|
||||||
|
`tags: ${tagList}`,
|
||||||
|
"---",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
return `${frontmatter}\n\n${body}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("=== Computer Guru Show Episode Importer ===\n");
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
if (!existsSync(EPISODES_DIR)) {
|
||||||
|
mkdirSync(EPISODES_DIR, { recursive: true });
|
||||||
|
console.log(`Created directory: ${EPISODES_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and parse
|
||||||
|
const xml = await fetchFeed(FEED_URL);
|
||||||
|
const items = parseFeed(xml);
|
||||||
|
|
||||||
|
// Process episodes
|
||||||
|
const turndown = createTurndown();
|
||||||
|
const warnings = [];
|
||||||
|
const episodes = [];
|
||||||
|
const filenames = new Set();
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const ep = processItem(items[i], i, warnings);
|
||||||
|
episodes.push(ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by season then episode (oldest first)
|
||||||
|
episodes.sort((a, b) => {
|
||||||
|
if (a.season !== b.season) return a.season - b.season;
|
||||||
|
return a.episode - b.episode;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write files
|
||||||
|
let written = 0;
|
||||||
|
let skippedDuplicates = 0;
|
||||||
|
|
||||||
|
for (const ep of episodes) {
|
||||||
|
let filename = episodeFilename(ep.season, ep.episode, ep.title);
|
||||||
|
|
||||||
|
// Handle duplicate filenames
|
||||||
|
if (filenames.has(filename)) {
|
||||||
|
const suffix = `-${Date.now().toString(36).slice(-4)}`;
|
||||||
|
filename = filename.replace(/\.md$/, `${suffix}.md`);
|
||||||
|
warnings.push(
|
||||||
|
`[WARNING] Duplicate filename resolved: ${filename} for "${ep.title}"`
|
||||||
|
);
|
||||||
|
skippedDuplicates++;
|
||||||
|
}
|
||||||
|
filenames.add(filename);
|
||||||
|
|
||||||
|
const markdown = generateMarkdown(ep, turndown);
|
||||||
|
const filepath = join(EPISODES_DIR, filename);
|
||||||
|
writeFileSync(filepath, markdown, "utf-8");
|
||||||
|
written++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report
|
||||||
|
console.log("\n=== Import Complete ===");
|
||||||
|
console.log(`Total episodes in feed: ${items.length}`);
|
||||||
|
console.log(`Files written: ${written}`);
|
||||||
|
|
||||||
|
if (skippedDuplicates > 0) {
|
||||||
|
console.log(`Duplicate filenames resolved: ${skippedDuplicates}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
console.log(`\n--- Warnings (${warnings.length}) ---`);
|
||||||
|
for (const w of warnings) {
|
||||||
|
console.log(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a sample of generated files
|
||||||
|
console.log("\n--- Sample Output (first 3 files) ---");
|
||||||
|
const sampleEpisodes = episodes.slice(0, 3);
|
||||||
|
for (const ep of sampleEpisodes) {
|
||||||
|
const filename = episodeFilename(ep.season, ep.episode, ep.title);
|
||||||
|
console.log(`\nFile: ${filename}`);
|
||||||
|
const markdown = generateMarkdown(ep, turndown);
|
||||||
|
// Show first 20 lines
|
||||||
|
const lines = markdown.split("\n").slice(0, 20);
|
||||||
|
console.log(lines.join("\n"));
|
||||||
|
if (markdown.split("\n").length > 20) {
|
||||||
|
console.log(" ...(truncated)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n[SUCCESS] Episode import finished.");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(`[ERROR] Import failed: ${err.message}`);
|
||||||
|
console.error(err.stack);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from 'astro:content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
episode: CollectionEntry<'episodes'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { episode } = Astro.props;
|
||||||
|
const { title, season, episode: episodeNum, pubDate, duration, tags, audioUrl } = episode.data;
|
||||||
|
|
||||||
|
const formattedDate = new Date(pubDate).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const seasonLabel = season === 0 ? 'Special' : `S${season}`;
|
||||||
|
const episodeLabel = `${seasonLabel} E${String(episodeNum).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
const description = episode.body
|
||||||
|
? episode.body.replace(/\\?\*\\?\*\\?\*/g, '').replace(/\\\[.*?\\\]/g, '').replace(/\[.*?\]/g, '').trim()
|
||||||
|
: '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="episode-card card"
|
||||||
|
data-season={String(season)}
|
||||||
|
data-tags={tags.join(',')}
|
||||||
|
data-title={title.toLowerCase()}
|
||||||
|
data-description={description.toLowerCase()}
|
||||||
|
>
|
||||||
|
<div class="episode-card__header">
|
||||||
|
<span class="badge">{episodeLabel}</span>
|
||||||
|
<span class="episode-card__duration">{duration}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href={`/episodes/${episode.id}`} class="episode-card__title-link">
|
||||||
|
<h3 class="episode-card__title">{title}</h3>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<time class="episode-card__date" datetime={pubDate.toISOString()}>
|
||||||
|
{formattedDate}
|
||||||
|
</time>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<p class="episode-card__description">{description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="episode-card__footer">
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<div class="episode-card__tags">
|
||||||
|
{tags.slice(0, 4).map((tag: string) => (
|
||||||
|
<span class="episode-card__tag">{tag}</span>
|
||||||
|
))}
|
||||||
|
{tags.length > 4 && (
|
||||||
|
<span class="episode-card__tag episode-card__tag--more">+{tags.length - 4}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn--primary episode-card__play"
|
||||||
|
data-audio-url={audioUrl}
|
||||||
|
data-episode-title={title}
|
||||||
|
data-season={String(season)}
|
||||||
|
data-episode-num={String(episodeNum)}
|
||||||
|
aria-label={`Play ${title}`}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||||
|
</svg>
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.episode-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition:
|
||||||
|
transform var(--transition-base),
|
||||||
|
box-shadow var(--transition-base),
|
||||||
|
border-color var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-left-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__duration {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__title-link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.episode-card__title-link:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__date {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__description {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__tag {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__tag--more {
|
||||||
|
background: var(--color-accent-glow);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__play {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card__play svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>('.episode-card__play').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const audioUrl = btn.dataset.audioUrl;
|
||||||
|
const title = btn.dataset.episodeTitle;
|
||||||
|
const season = parseInt(btn.dataset.season ?? '0', 10);
|
||||||
|
const episode = parseInt(btn.dataset.episodeNum ?? '0', 10);
|
||||||
|
|
||||||
|
if (audioUrl) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent('play-episode', {
|
||||||
|
detail: { audioUrl, title, season, episode },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
interface SeasonCount {
|
||||||
|
season: number;
|
||||||
|
count: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
seasonCounts: SeasonCount[];
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { seasonCounts, totalCount } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="season-filter" id="season-filter">
|
||||||
|
<div class="season-filter__scroll">
|
||||||
|
<button
|
||||||
|
class="season-filter__tab season-filter__tab--active"
|
||||||
|
data-season="all"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
All Episodes
|
||||||
|
<span class="season-filter__count">{totalCount}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{seasonCounts.map(({ season, count, label }) => (
|
||||||
|
<button
|
||||||
|
class="season-filter__tab"
|
||||||
|
data-season={String(season)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<span class="season-filter__count">{count}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.season-filter {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__scroll {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--color-border) transparent;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__scroll::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__scroll::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition:
|
||||||
|
color var(--transition-fast),
|
||||||
|
border-color var(--transition-fast);
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__tab:hover {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__tab--active {
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-bottom-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__count {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
padding: 1px var(--space-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-filter__tab--active .season-filter__count {
|
||||||
|
background: var(--color-accent-glow);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,919 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface EpisodeInfo {
|
||||||
|
audioUrl: string;
|
||||||
|
title: string;
|
||||||
|
season: number;
|
||||||
|
episode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayerState extends EpisodeInfo {
|
||||||
|
currentTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayEpisodeEvent extends CustomEvent {
|
||||||
|
detail: EpisodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'persistent-player-state';
|
||||||
|
const PLAYBACK_SPEEDS = [0.75, 1, 1.25, 1.5, 2] as const;
|
||||||
|
const SKIP_SECONDS = 15;
|
||||||
|
const SAVE_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
if (!isFinite(seconds) || seconds < 0) return '0:00';
|
||||||
|
const hrs = Math.floor(seconds / 3600);
|
||||||
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
const paddedSecs = secs.toString().padStart(2, '0');
|
||||||
|
if (hrs > 0) {
|
||||||
|
return `${hrs}:${mins.toString().padStart(2, '0')}:${paddedSecs}`;
|
||||||
|
}
|
||||||
|
return `${mins}:${paddedSecs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadState(): PlayerState | null {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw) as PlayerState;
|
||||||
|
if (parsed.audioUrl && parsed.title != null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState(state: PlayerState): void {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
|
} catch {
|
||||||
|
// sessionStorage may be unavailable or full; silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearState(): void {
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem(STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEpisodeLabel(season: number, episode: number): string {
|
||||||
|
if (season === 0) return 'Special';
|
||||||
|
return `S${season}E${String(episode).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function PersistentPlayer() {
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const progressBarRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const saveTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const [episodeInfo, setEpisodeInfo] = useState<EpisodeInfo | null>(null);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
const [buffered, setBuffered] = useState(0);
|
||||||
|
const [volume, setVolume] = useState(1);
|
||||||
|
const [speedIndex, setSpeedIndex] = useState(1); // default 1x
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [isDraggingProgress, setIsDraggingProgress] = useState(false);
|
||||||
|
|
||||||
|
// ── Restore state on mount ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = loadState();
|
||||||
|
if (saved) {
|
||||||
|
setEpisodeInfo({
|
||||||
|
audioUrl: saved.audioUrl,
|
||||||
|
title: saved.title,
|
||||||
|
season: saved.season,
|
||||||
|
episode: saved.episode,
|
||||||
|
});
|
||||||
|
setCurrentTime(saved.currentTime);
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── Listen for play-episode custom events ───────────────────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handlePlayEpisode(e: Event) {
|
||||||
|
const event = e as PlayEpisodeEvent;
|
||||||
|
const { audioUrl, title, season, episode } = event.detail;
|
||||||
|
if (!audioUrl) return;
|
||||||
|
|
||||||
|
setEpisodeInfo({ audioUrl, title, season, episode });
|
||||||
|
setCurrentTime(0);
|
||||||
|
setDuration(0);
|
||||||
|
setBuffered(0);
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
// Playback will start once the audio element loads the new source
|
||||||
|
// (handled in the audioUrl effect below)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('play-episode', handlePlayEpisode);
|
||||||
|
return () => document.removeEventListener('play-episode', handlePlayEpisode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── Wire up audio element when episode changes ──────────────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio || !episodeInfo) return;
|
||||||
|
|
||||||
|
// If source changed, load new source
|
||||||
|
if (audio.src !== episodeInfo.audioUrl) {
|
||||||
|
audio.src = episodeInfo.audioUrl;
|
||||||
|
audio.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore saved position for restored state (non-zero currentTime means restored)
|
||||||
|
if (currentTime > 0 && audio.readyState >= 1) {
|
||||||
|
audio.currentTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-play if this is a fresh play-episode event (currentTime === 0)
|
||||||
|
// For restored sessions, don't auto-play
|
||||||
|
const isRestoredSession = currentTime > 0;
|
||||||
|
if (!isRestoredSession) {
|
||||||
|
const playOnReady = () => {
|
||||||
|
audio.play().catch(() => {
|
||||||
|
// Browser may block autoplay; user will click play manually
|
||||||
|
});
|
||||||
|
audio.removeEventListener('canplay', playOnReady);
|
||||||
|
};
|
||||||
|
if (audio.readyState >= 3) {
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
} else {
|
||||||
|
audio.addEventListener('canplay', playOnReady);
|
||||||
|
return () => audio.removeEventListener('canplay', playOnReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [episodeInfo?.audioUrl]);
|
||||||
|
|
||||||
|
// ── Restore currentTime once audio metadata is loaded ───────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio || !episodeInfo) return;
|
||||||
|
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
const saved = loadState();
|
||||||
|
if (saved && saved.audioUrl === episodeInfo.audioUrl && saved.currentTime > 0) {
|
||||||
|
audio.currentTime = saved.currentTime;
|
||||||
|
setCurrentTime(saved.currentTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
}, [episodeInfo]);
|
||||||
|
|
||||||
|
// ── Audio event handlers ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
|
||||||
|
const onPlay = () => setIsPlaying(true);
|
||||||
|
const onPause = () => setIsPlaying(false);
|
||||||
|
const onEnded = () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
setCurrentTime(0);
|
||||||
|
};
|
||||||
|
const onTimeUpdate = () => {
|
||||||
|
if (!isDraggingProgress) {
|
||||||
|
setCurrentTime(audio.currentTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onDurationChange = () => {
|
||||||
|
if (isFinite(audio.duration)) {
|
||||||
|
setDuration(audio.duration);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onProgress = () => {
|
||||||
|
if (audio.buffered.length > 0) {
|
||||||
|
setBuffered(audio.buffered.end(audio.buffered.length - 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener('play', onPlay);
|
||||||
|
audio.addEventListener('pause', onPause);
|
||||||
|
audio.addEventListener('ended', onEnded);
|
||||||
|
audio.addEventListener('timeupdate', onTimeUpdate);
|
||||||
|
audio.addEventListener('durationchange', onDurationChange);
|
||||||
|
audio.addEventListener('progress', onProgress);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
audio.removeEventListener('play', onPlay);
|
||||||
|
audio.removeEventListener('pause', onPause);
|
||||||
|
audio.removeEventListener('ended', onEnded);
|
||||||
|
audio.removeEventListener('timeupdate', onTimeUpdate);
|
||||||
|
audio.removeEventListener('durationchange', onDurationChange);
|
||||||
|
audio.removeEventListener('progress', onProgress);
|
||||||
|
};
|
||||||
|
}, [isDraggingProgress]);
|
||||||
|
|
||||||
|
// ── Periodic state persistence ──────────────────────────────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!episodeInfo || !isPlaying) {
|
||||||
|
if (saveTimerRef.current != null) {
|
||||||
|
window.clearInterval(saveTimerRef.current);
|
||||||
|
saveTimerRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTimerRef.current = window.setInterval(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (audio && episodeInfo) {
|
||||||
|
saveState({
|
||||||
|
...episodeInfo,
|
||||||
|
currentTime: audio.currentTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, SAVE_INTERVAL_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (saveTimerRef.current != null) {
|
||||||
|
window.clearInterval(saveTimerRef.current);
|
||||||
|
saveTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [episodeInfo, isPlaying]);
|
||||||
|
|
||||||
|
// ── Save state on pause / before unload ─────────────────────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleBeforeUnload() {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (audio && episodeInfo) {
|
||||||
|
saveState({ ...episodeInfo, currentTime: audio.currentTime });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
}, [episodeInfo]);
|
||||||
|
|
||||||
|
// ── Controls ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const togglePlayPause = useCallback(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
if (audio.paused) {
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
} else {
|
||||||
|
audio.pause();
|
||||||
|
if (episodeInfo) {
|
||||||
|
saveState({ ...episodeInfo, currentTime: audio.currentTime });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [episodeInfo]);
|
||||||
|
|
||||||
|
const skipBack = useCallback(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
audio.currentTime = Math.max(0, audio.currentTime - SKIP_SECONDS);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const skipForward = useCallback(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + SKIP_SECONDS);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cycleSpeed = useCallback(() => {
|
||||||
|
setSpeedIndex((prev) => {
|
||||||
|
const next = (prev + 1) % PLAYBACK_SPEEDS.length;
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (audio) {
|
||||||
|
audio.playbackRate = PLAYBACK_SPEEDS[next];
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const val = parseFloat(e.target.value);
|
||||||
|
setVolume(val);
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (audio) {
|
||||||
|
audio.volume = val;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleProgressClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const bar = progressBarRef.current;
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!bar || !audio || !duration) return;
|
||||||
|
|
||||||
|
const rect = bar.getBoundingClientRect();
|
||||||
|
const fraction = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
||||||
|
const newTime = fraction * duration;
|
||||||
|
audio.currentTime = newTime;
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
},
|
||||||
|
[duration],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProgressMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
setIsDraggingProgress(true);
|
||||||
|
handleProgressClick(e);
|
||||||
|
|
||||||
|
const handleMouseMove = (ev: MouseEvent) => {
|
||||||
|
const bar = progressBarRef.current;
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!bar || !audio || !duration) return;
|
||||||
|
const rect = bar.getBoundingClientRect();
|
||||||
|
const fraction = Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width));
|
||||||
|
const newTime = fraction * duration;
|
||||||
|
audio.currentTime = newTime;
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsDraggingProgress(false);
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
},
|
||||||
|
[duration, handleProgressClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
const closePlayer = useCallback(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (audio) {
|
||||||
|
audio.pause();
|
||||||
|
audio.removeAttribute('src');
|
||||||
|
audio.load();
|
||||||
|
}
|
||||||
|
setIsPlaying(false);
|
||||||
|
setEpisodeInfo(null);
|
||||||
|
setCurrentTime(0);
|
||||||
|
setDuration(0);
|
||||||
|
setBuffered(0);
|
||||||
|
setIsVisible(false);
|
||||||
|
clearState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── Keyboard shortcuts ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!episodeInfo) return;
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
// Only respond when not typing in an input
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (
|
||||||
|
target.tagName === 'INPUT' ||
|
||||||
|
target.tagName === 'TEXTAREA' ||
|
||||||
|
target.tagName === 'SELECT' ||
|
||||||
|
target.isContentEditable
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
togglePlayPause();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
e.preventDefault();
|
||||||
|
skipBack();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
e.preventDefault();
|
||||||
|
skipForward();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [episodeInfo, togglePlayPause, skipBack, skipForward]);
|
||||||
|
|
||||||
|
// ── Derived values ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||||
|
const bufferedPercent = duration > 0 ? (buffered / duration) * 100 : 0;
|
||||||
|
const currentSpeed = PLAYBACK_SPEEDS[speedIndex];
|
||||||
|
const episodeLabel = episodeInfo ? buildEpisodeLabel(episodeInfo.season, episodeInfo.episode) : '';
|
||||||
|
|
||||||
|
// ── Render ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Hidden audio element - always mounted so it survives re-renders */}
|
||||||
|
<audio ref={audioRef} preload="metadata" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="persistent-player"
|
||||||
|
style={{
|
||||||
|
transform: isVisible ? 'translateY(0)' : 'translateY(100%)',
|
||||||
|
}}
|
||||||
|
role="region"
|
||||||
|
aria-label="Audio player"
|
||||||
|
>
|
||||||
|
{/* Progress bar - full width across top of player */}
|
||||||
|
<div
|
||||||
|
className="persistent-player__progress-bar"
|
||||||
|
ref={progressBarRef}
|
||||||
|
onClick={handleProgressClick}
|
||||||
|
onMouseDown={handleProgressMouseDown}
|
||||||
|
role="slider"
|
||||||
|
aria-label="Playback progress"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={duration}
|
||||||
|
aria-valuenow={currentTime}
|
||||||
|
aria-valuetext={`${formatTime(currentTime)} of ${formatTime(duration)}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="persistent-player__progress-buffered"
|
||||||
|
style={{ width: `${bufferedPercent}%` }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="persistent-player__progress-fill"
|
||||||
|
style={{ width: `${progressPercent}%` }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="persistent-player__progress-handle"
|
||||||
|
style={{ left: `${progressPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="persistent-player__content">
|
||||||
|
{/* Left: Episode info */}
|
||||||
|
<div className="persistent-player__info">
|
||||||
|
{episodeLabel && (
|
||||||
|
<span className="persistent-player__badge">{episodeLabel}</span>
|
||||||
|
)}
|
||||||
|
<span className="persistent-player__title" title={episodeInfo?.title ?? ''}>
|
||||||
|
{episodeInfo?.title ?? ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center: Playback controls */}
|
||||||
|
<div className="persistent-player__controls">
|
||||||
|
<button
|
||||||
|
className="persistent-player__btn persistent-player__btn--skip"
|
||||||
|
onClick={skipBack}
|
||||||
|
aria-label="Skip back 15 seconds"
|
||||||
|
title="Skip back 15 seconds"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="1 4 1 10 7 10" />
|
||||||
|
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
||||||
|
<text x="12" y="16" textAnchor="middle" fill="currentColor" stroke="none" fontSize="8" fontWeight="700">15</text>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="persistent-player__btn persistent-player__btn--play"
|
||||||
|
onClick={togglePlayPause}
|
||||||
|
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||||
|
title={isPlaying ? 'Pause' : 'Play'}
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<rect x="6" y="4" width="4" height="16" rx="1" />
|
||||||
|
<rect x="14" y="4" width="4" height="16" rx="1" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<polygon points="6 3 20 12 6 21 6 3" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="persistent-player__btn persistent-player__btn--skip"
|
||||||
|
onClick={skipForward}
|
||||||
|
aria-label="Skip forward 15 seconds"
|
||||||
|
title="Skip forward 15 seconds"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="23 4 23 10 17 10" />
|
||||||
|
<path d="M20.49 15a9 9 0 1 1-2.13-9.36L23 10" />
|
||||||
|
<text x="12" y="16" textAnchor="middle" fill="currentColor" stroke="none" fontSize="8" fontWeight="700">15</text>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time display */}
|
||||||
|
<div className="persistent-player__time">
|
||||||
|
<span>{formatTime(currentTime)}</span>
|
||||||
|
<span className="persistent-player__time-separator">/</span>
|
||||||
|
<span>{formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Volume, speed, close */}
|
||||||
|
<div className="persistent-player__right">
|
||||||
|
<div className="persistent-player__volume">
|
||||||
|
<button
|
||||||
|
className="persistent-player__btn persistent-player__btn--icon"
|
||||||
|
onClick={() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
if (audio.volume > 0) {
|
||||||
|
audio.volume = 0;
|
||||||
|
setVolume(0);
|
||||||
|
} else {
|
||||||
|
audio.volume = 1;
|
||||||
|
setVolume(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={volume === 0 ? 'Unmute' : 'Mute'}
|
||||||
|
title={volume === 0 ? 'Unmute' : 'Mute'}
|
||||||
|
>
|
||||||
|
{volume === 0 ? (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
|
<line x1="23" y1="9" x2="17" y2="15" />
|
||||||
|
<line x1="17" y1="9" x2="23" y2="15" />
|
||||||
|
</svg>
|
||||||
|
) : volume < 0.5 ? (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
||||||
|
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className="persistent-player__volume-slider"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
value={volume}
|
||||||
|
onChange={handleVolumeChange}
|
||||||
|
aria-label="Volume"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="persistent-player__btn persistent-player__btn--speed"
|
||||||
|
onClick={cycleSpeed}
|
||||||
|
aria-label={`Playback speed: ${currentSpeed}x`}
|
||||||
|
title={`Playback speed: ${currentSpeed}x`}
|
||||||
|
>
|
||||||
|
{currentSpeed}x
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="persistent-player__btn persistent-player__btn--close"
|
||||||
|
onClick={closePlayer}
|
||||||
|
aria-label="Close player"
|
||||||
|
title="Close player"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.persistent-player {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
height: 72px;
|
||||||
|
background: hsl(220 20% 6% / 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-top: 1px solid hsl(200 85% 55% / 0.15);
|
||||||
|
transition: transform 350ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .persistent-player {
|
||||||
|
background: hsl(220 20% 97% / 0.92);
|
||||||
|
border-top-color: hsl(200 85% 42% / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress bar ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.persistent-player__progress-bar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background: hsl(220 12% 24% / 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__progress-bar:hover {
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__progress-bar:hover .persistent-player__progress-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .persistent-player__progress-bar {
|
||||||
|
background: hsl(220 12% 85% / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__progress-buffered {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: hsl(220 8% 48% / 0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__progress-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-accent, hsl(200 85% 55%));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__progress-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent, hsl(200 85% 55%));
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Content layout ───────────────────────────────────── */
|
||||||
|
|
||||||
|
.persistent-player__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 16px;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Episode info ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.persistent-player__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: hsl(200 85% 55% / 0.2);
|
||||||
|
color: var(--color-accent, hsl(200 85% 55%));
|
||||||
|
border: 1px solid hsl(200 85% 55% / 0.3);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-primary, hsl(220 10% 92%));
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Controls ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.persistent-player__controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-secondary, hsl(220 8% 68%));
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: color 150ms ease, background 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn:hover {
|
||||||
|
color: var(--color-text-primary, hsl(220 10% 92%));
|
||||||
|
background: hsl(220 16% 16% / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .persistent-player__btn:hover {
|
||||||
|
background: hsl(220 16% 92% / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--play {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: hsl(220 20% 8%);
|
||||||
|
background: var(--color-accent, hsl(200 85% 55%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--play:hover {
|
||||||
|
color: hsl(220 20% 8%);
|
||||||
|
background: var(--color-accent-hover, hsl(200 85% 65%));
|
||||||
|
box-shadow: 0 0 16px hsl(200 85% 55% / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--speed {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
min-width: 36px;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--close {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Time display ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.persistent-player__time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
color: var(--color-text-muted, hsl(220 6% 48%));
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__time-separator {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Volume ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.persistent-player__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__volume {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__volume-slider {
|
||||||
|
width: 72px;
|
||||||
|
height: 4px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: hsl(220 12% 24%);
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .persistent-player__volume-slider {
|
||||||
|
background: hsl(220 12% 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__volume-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent, hsl(200 85% 55%));
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__volume-slider::-moz-range-thumb {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent, hsl(200 85% 55%));
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile responsive ────────────────────────────────── */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.persistent-player {
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__content {
|
||||||
|
padding: 0 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__volume {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--speed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__time {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--play {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--play svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__btn--skip svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.persistent-player__btn--skip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persistent-player__title {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="section about-preview fade-in">
|
||||||
|
<div class="container">
|
||||||
|
<div class="about-grid">
|
||||||
|
<div class="about-text">
|
||||||
|
<span class="badge">About the Show</span>
|
||||||
|
<h2 class="about-title">Meet Your Host</h2>
|
||||||
|
<p class="about-lead">
|
||||||
|
Mike Swanson has been breaking down technology for everyday people since 2014.
|
||||||
|
As a Tucson-based tech professional and broadcaster, he brings decades of
|
||||||
|
hands-on experience to every episode.
|
||||||
|
</p>
|
||||||
|
<blockquote class="about-quote">
|
||||||
|
"Technology should empower you, not intimidate you. That is what this show is all about."
|
||||||
|
</blockquote>
|
||||||
|
<p class="about-body">
|
||||||
|
From cybersecurity and privacy to the latest gadgets and internet culture,
|
||||||
|
The Computer Guru Show covers it all -- with a healthy dose of humor, real-world
|
||||||
|
advice, and honest opinions. No corporate sponsors telling us what to say.
|
||||||
|
Just straight talk about tech.
|
||||||
|
</p>
|
||||||
|
<a href="/about" class="btn btn--primary">
|
||||||
|
Learn More
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
<polyline points="12 5 19 12 12 19"></polyline>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-visual">
|
||||||
|
<div class="about-image-placeholder">
|
||||||
|
<div class="about-image-inner">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
||||||
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||||
|
<line x1="12" y1="19" x2="12" y2="23"></line>
|
||||||
|
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||||
|
</svg>
|
||||||
|
<span class="about-image-text">The Computer Guru</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="about-visual-accent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.about-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-12);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-title {
|
||||||
|
font-size: var(--text-3xl);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-lead {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-quote {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--color-accent);
|
||||||
|
padding-left: var(--space-6);
|
||||||
|
border-left: 3px solid var(--color-accent);
|
||||||
|
margin-block: var(--space-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-body {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-text .btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visual / placeholder area */
|
||||||
|
.about-visual {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-image-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 4 / 5;
|
||||||
|
max-width: 400px;
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-image-placeholder::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, hsl(200 85% 55% / 0.05), transparent 50%),
|
||||||
|
linear-gradient(315deg, hsl(270 60% 50% / 0.03), transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-image-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-image-text {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-visual-accent {
|
||||||
|
position: absolute;
|
||||||
|
width: 120%;
|
||||||
|
height: 120%;
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
border: 1px solid hsl(200 85% 55% / 0.06);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
transform: rotate(3deg);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.about-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-visual {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-image-placeholder {
|
||||||
|
max-width: 280px;
|
||||||
|
margin-inline: auto;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
const allPosts = await getCollection('blog');
|
||||||
|
const publishedPosts = allPosts
|
||||||
|
.filter((post) => !post.data.draft)
|
||||||
|
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
const isSingle = publishedPosts.length === 1;
|
||||||
|
---
|
||||||
|
|
||||||
|
{publishedPosts.length > 0 && (
|
||||||
|
<section class="section blog-highlights fade-in">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section__title">From the Blog</h2>
|
||||||
|
<a href="/blog" class="section-header__link">
|
||||||
|
All Posts
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class:list={['blog-grid', { 'blog-grid--single': isSingle }]}>
|
||||||
|
{publishedPosts.map((post) => {
|
||||||
|
const formattedDate = new Date(post.data.pubDate).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article class="blog-card card">
|
||||||
|
<div class="blog-card__meta">
|
||||||
|
<time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
|
||||||
|
{post.data.tags.length > 0 && (
|
||||||
|
<div class="blog-card__tags">
|
||||||
|
{post.data.tags.slice(0, 2).map((tag: string) => (
|
||||||
|
<span class="blog-card__tag">{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href={`/blog/${post.id}`} class="blog-card__title-link">
|
||||||
|
<h3 class="blog-card__title">{post.data.title}</h3>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="blog-card__description">{post.data.description}</p>
|
||||||
|
|
||||||
|
<div class="blog-card__footer">
|
||||||
|
<span class="blog-card__author">By {post.data.author}</span>
|
||||||
|
<a href={`/blog/${post.id}`} class="blog-card__read-more">
|
||||||
|
Read More
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
<polyline points="12 5 19 12 12 19"></polyline>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent);
|
||||||
|
transition: gap var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header__link:hover {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-grid--single {
|
||||||
|
max-width: 600px;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__meta time {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__tags {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__tag {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--color-accent-glow);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__title-link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__title-link:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__description {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__author {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__read-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent);
|
||||||
|
transition: gap var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card__read-more:hover {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
const allEpisodes = await getCollection('episodes');
|
||||||
|
|
||||||
|
const classicEpisodes = allEpisodes.filter((ep) => ep.data.classic);
|
||||||
|
|
||||||
|
// If no episodes marked as classic, pick a curated selection:
|
||||||
|
// the first episode of each season for variety
|
||||||
|
const fallbackEpisodes = (() => {
|
||||||
|
const seasonFirsts = new Map<number, typeof allEpisodes[0]>();
|
||||||
|
for (const ep of allEpisodes) {
|
||||||
|
const s = ep.data.season;
|
||||||
|
if (s === 0) continue;
|
||||||
|
if (!seasonFirsts.has(s) || ep.data.episode < seasonFirsts.get(s)!.data.episode) {
|
||||||
|
seasonFirsts.set(s, ep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(seasonFirsts.values())
|
||||||
|
.sort((a, b) => b.data.season - a.data.season)
|
||||||
|
.slice(0, 5);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const displayEpisodes = classicEpisodes.length > 0 ? classicEpisodes : fallbackEpisodes;
|
||||||
|
|
||||||
|
const hasClassics = classicEpisodes.length > 0;
|
||||||
|
---
|
||||||
|
|
||||||
|
{displayEpisodes.length > 0 && (
|
||||||
|
<section class="section classics fade-in">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section__title">
|
||||||
|
{hasClassics ? 'Fan Favorites' : 'From the Archive'}
|
||||||
|
</h2>
|
||||||
|
<a href="/episodes" class="section-header__link">
|
||||||
|
Browse All Episodes
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="classics-scroll">
|
||||||
|
<div class="classics-track">
|
||||||
|
{displayEpisodes.map((episode) => {
|
||||||
|
const { title, season, episode: episodeNum, pubDate, duration } = episode.data;
|
||||||
|
const seasonLabel = season === 0 ? 'Special' : `S${season}`;
|
||||||
|
const episodeLabel = `${seasonLabel} E${String(episodeNum).padStart(2, '0')}`;
|
||||||
|
const formattedDate = new Date(pubDate).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
const description = episode.body
|
||||||
|
? episode.body.replace(/\\?\*\\?\*\\?\*/g, '').replace(/\\\[.*?\\\]/g, '').replace(/\[.*?\]/g, '').trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={`/episodes/${episode.id}`} class="classic-card">
|
||||||
|
{hasClassics && <span class="classic-card__badge">Classic</span>}
|
||||||
|
{!hasClassics && <span class="classic-card__badge classic-card__badge--season">Season {season}</span>}
|
||||||
|
<div class="classic-card__meta">
|
||||||
|
<span class="badge">{episodeLabel}</span>
|
||||||
|
<span class="classic-card__duration">{duration}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="classic-card__title">{title}</h3>
|
||||||
|
{description && (
|
||||||
|
<p class="classic-card__desc">{description.slice(0, 100)}{description.length > 100 ? '...' : ''}</p>
|
||||||
|
)}
|
||||||
|
<time class="classic-card__date" datetime={pubDate.toISOString()}>{formattedDate}</time>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent);
|
||||||
|
transition: gap var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header__link:hover {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classics-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-inline: calc(-1 * var(--space-6));
|
||||||
|
padding-inline: var(--space-6);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--color-border) transparent;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classics-scroll::-webkit-scrollbar {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classics-scroll::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classics-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classics-track {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-6);
|
||||||
|
padding-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card {
|
||||||
|
flex: 0 0 280px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-6);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
transform var(--transition-base),
|
||||||
|
box-shadow var(--transition-base),
|
||||||
|
border-color var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card__badge {
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: hsl(40 90% 55% / 0.15);
|
||||||
|
color: var(--color-warning);
|
||||||
|
border: 1px solid hsl(40 90% 55% / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card__badge--season {
|
||||||
|
background: var(--color-accent-glow);
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-color: hsl(200 85% 55% / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card__duration {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card__title {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card__desc {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-card__date {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.classic-card {
|
||||||
|
flex: 0 0 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-bg-effects">
|
||||||
|
<div class="hero-glow hero-glow--primary"></div>
|
||||||
|
<div class="hero-glow hero-glow--secondary"></div>
|
||||||
|
<div class="hero-ring"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-content container">
|
||||||
|
<span class="badge hero-badge">Returning 2026</span>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<span class="hero-title__line hero-title__line--1">The Computer</span>
|
||||||
|
<span class="hero-title__line hero-title__line--2">Guru Show</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero-tagline">Technology: Fun and Simple</p>
|
||||||
|
<p class="hero-description">
|
||||||
|
Your source for making sense of the tech world without the jargon.
|
||||||
|
Hosted by Mike Swanson from Tucson, Arizona -- cutting through the noise
|
||||||
|
so you can enjoy technology the way it was meant to be.
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href="/episodes" class="btn btn--primary btn--lg">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none"></polygon>
|
||||||
|
</svg>
|
||||||
|
Browse Episodes
|
||||||
|
</a>
|
||||||
|
<a href="/subscribe" class="btn btn--ghost btn--lg">Subscribe</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-scroll-hint" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="7 13 12 18 17 13"></polyline>
|
||||||
|
<polyline points="7 6 12 11 17 6"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, hsl(220 30% 6% / 0.95), hsl(200 40% 8% / 0.9)),
|
||||||
|
radial-gradient(ellipse at 20% 50%, hsl(200 85% 55% / 0.12), transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, hsl(270 60% 50% / 0.08), transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .hero {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, hsl(220 20% 95% / 0.95), hsl(200 30% 92% / 0.9)),
|
||||||
|
radial-gradient(ellipse at 20% 50%, hsl(200 85% 55% / 0.08), transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, hsl(270 60% 50% / 0.05), transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background effects */
|
||||||
|
.hero-bg-effects {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(80px);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow--primary {
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
top: -200px;
|
||||||
|
right: -100px;
|
||||||
|
background: radial-gradient(circle, hsl(200 85% 55% / 0.15), transparent 70%);
|
||||||
|
animation: glowDrift 8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow--secondary {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
bottom: -100px;
|
||||||
|
left: -50px;
|
||||||
|
background: radial-gradient(circle, hsl(270 60% 50% / 0.1), transparent 70%);
|
||||||
|
animation: glowDrift 10s ease-in-out infinite alternate-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-ring {
|
||||||
|
position: absolute;
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
top: 50%;
|
||||||
|
right: 5%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
border: 1px solid hsl(200 85% 55% / 0.08);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ringPulse 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-ring::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 40px;
|
||||||
|
border: 1px solid hsl(200 85% 55% / 0.06);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ringPulse 4s ease-in-out infinite 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-ring::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 80px;
|
||||||
|
border: 1px solid hsl(200 85% 55% / 0.04);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ringPulse 4s ease-in-out infinite 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .hero-glow--primary {
|
||||||
|
background: radial-gradient(circle, hsl(200 85% 55% / 0.08), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .hero-glow--secondary {
|
||||||
|
background: radial-gradient(circle, hsl(270 60% 50% / 0.06), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .hero-ring {
|
||||||
|
border-color: hsl(200 85% 55% / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .hero-ring::before {
|
||||||
|
border-color: hsl(200 85% 55% / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .hero-ring::after {
|
||||||
|
border-color: hsl(200 85% 55% / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
animation: fadeSlideIn 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title__line {
|
||||||
|
display: block;
|
||||||
|
font-size: clamp(2.5rem, 6vw, 5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.05;
|
||||||
|
background: linear-gradient(135deg, var(--color-text-primary) 30%, var(--color-accent) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title__line--1 {
|
||||||
|
animation: fadeSlideIn 0.6s ease-out 0.15s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title__line--2 {
|
||||||
|
animation: fadeSlideIn 0.6s ease-out 0.3s both;
|
||||||
|
background: linear-gradient(135deg, var(--color-accent) 0%, hsl(220 70% 65%) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tagline {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
animation: fadeSlideIn 0.6s ease-out 0.45s both;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 560px;
|
||||||
|
animation: fadeSlideIn 0.6s ease-out 0.6s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-top: var(--space-8);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
animation: fadeSlideIn 0.6s ease-out 0.75s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--lg {
|
||||||
|
padding: var(--space-4) var(--space-8);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll hint */
|
||||||
|
.hero-scroll-hint {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--space-8);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
animation: bounceDown 2s ease-in-out infinite;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyframes */
|
||||||
|
@keyframes fadeSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glowDrift {
|
||||||
|
from {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(30px, -20px) scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ringPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(0, -50%) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(0, -50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounceDown {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-50%) translateY(8px);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero {
|
||||||
|
min-height: auto;
|
||||||
|
padding-block: var(--space-16) var(--space-24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-ring {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
right: -80px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-scroll-hint {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hero-title__line {
|
||||||
|
font-size: clamp(2rem, 10vw, 3rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import EpisodeCard from '../episodes/EpisodeCard.astro';
|
||||||
|
|
||||||
|
const allEpisodes = await getCollection('episodes');
|
||||||
|
const latestEpisodes = allEpisodes
|
||||||
|
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||||
|
.slice(0, 6);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="section latest-episodes fade-in">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section__title">Latest Episodes</h2>
|
||||||
|
<a href="/episodes" class="section-header__link">
|
||||||
|
View All
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{latestEpisodes.length > 0 ? (
|
||||||
|
<div class="grid grid--3">
|
||||||
|
{latestEpisodes.map((episode) => (
|
||||||
|
<EpisodeCard episode={episode} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>New episodes coming soon. Stay tuned for the return of The Computer Guru Show.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent);
|
||||||
|
transition: gap var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header__link:hover {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-16) var(--space-8);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px dashed var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
const allEpisodes = await getCollection('episodes');
|
||||||
|
const totalEpisodes = allEpisodes.length;
|
||||||
|
|
||||||
|
const seasons = new Set(allEpisodes.map((ep) => ep.data.season).filter((s) => s > 0));
|
||||||
|
const totalSeasons = seasons.size;
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ value: `${totalEpisodes}+`, label: 'Episodes', sublabel: 'and counting' },
|
||||||
|
{ value: String(totalSeasons), label: 'Seasons', sublabel: 'on the air' },
|
||||||
|
{ value: '2014', label: 'Since', sublabel: 'year one' },
|
||||||
|
{ value: 'Tucson', label: 'Arizona', sublabel: 'home base' },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="section stats fade-in">
|
||||||
|
<div class="container">
|
||||||
|
<div class="stats-grid">
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-card__value">{stat.value}</span>
|
||||||
|
<span class="stat-card__label">{stat.label}</span>
|
||||||
|
<span class="stat-card__sublabel">{stat.sublabel}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.stats {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at 50% 50%, hsl(200 85% 55% / 0.04), transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-8) var(--space-6);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
transform var(--transition-base),
|
||||||
|
box-shadow var(--transition-base),
|
||||||
|
border-color var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-accent);
|
||||||
|
transition: width var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-md),
|
||||||
|
0 0 30px hsl(200 85% 55% / 0.08);
|
||||||
|
border-color: hsl(200 85% 55% / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover::after {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__value {
|
||||||
|
font-size: clamp(var(--text-2xl), 4vw, var(--text-4xl));
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
background: linear-gradient(135deg, var(--color-text-primary), var(--color-accent));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__label {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__sublabel {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
import { platforms } from '../../data/platforms';
|
||||||
|
|
||||||
|
const platformIcons: Record<string, string> = {
|
||||||
|
apple: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
|
||||||
|
spotify: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>`,
|
||||||
|
google: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.372 0 0 5.373 0 12s5.372 12 12 12c6.627 0 12-5.373 12-12S18.627 0 12 0zm-.5 4.5h1v3.258l2.82-1.628.5.866-2.82 1.628 2.82 1.628-.5.866L12.5 9.49V12.5h-1V9.49l-2.82 1.628-.5-.866 2.82-1.628-2.82-1.628.5-.866L11.5 7.758V4.5z"/></svg>`,
|
||||||
|
overcast: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="5" x2="12" y2="7" stroke="currentColor" stroke-width="2"/><line x1="12" y1="17" x2="12" y2="19" stroke="currentColor" stroke-width="2"/><line x1="5" y1="12" x2="7" y2="12" stroke="currentColor" stroke-width="2"/><line x1="17" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2"/></svg>`,
|
||||||
|
pocketcasts: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.372 0 0 5.372 0 12s5.372 12 12 12 12-5.372 12-12S18.628 0 12 0zm0 3.6c4.636 0 8.4 3.764 8.4 8.4h-2.4c0-3.312-2.688-6-6-6s-6 2.688-6 6 2.688 6 6 6v2.4c-4.636 0-8.4-3.764-8.4-8.4S7.364 3.6 12 3.6zm0 4.8a3.6 3.6 0 110 7.2 3.6 3.6 0 010-7.2z"/></svg>`,
|
||||||
|
rss: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1" fill="currentColor"></circle></svg>`,
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="subscribe-cta fade-in">
|
||||||
|
<div class="subscribe-cta__bg"></div>
|
||||||
|
<div class="container subscribe-cta__content">
|
||||||
|
<h2 class="subscribe-cta__title">Never Miss an Episode</h2>
|
||||||
|
<p class="subscribe-cta__subtitle">
|
||||||
|
Subscribe on your favorite platform and get notified when new episodes drop.
|
||||||
|
</p>
|
||||||
|
<div class="subscribe-cta__platforms">
|
||||||
|
{platforms.map((platform) => (
|
||||||
|
<a
|
||||||
|
href={platform.url}
|
||||||
|
class="subscribe-cta__platform"
|
||||||
|
target={platform.url.startsWith('http') ? '_blank' : undefined}
|
||||||
|
rel={platform.url.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||||
|
aria-label={`Subscribe on ${platform.name}`}
|
||||||
|
>
|
||||||
|
<span class="subscribe-cta__icon" set:html={platformIcons[platform.icon] || platformIcons.rss} />
|
||||||
|
<span class="subscribe-cta__platform-name">{platform.name}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.subscribe-cta {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-block: var(--space-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, hsl(200 85% 55% / 0.08), hsl(270 60% 50% / 0.05)),
|
||||||
|
linear-gradient(to bottom, var(--color-bg-secondary), var(--color-bg-primary));
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__bg::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(to right, transparent, var(--color-accent), transparent);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__bg::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(to right, transparent, var(--color-accent), transparent);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__title {
|
||||||
|
font-size: var(--text-3xl);
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(135deg, var(--color-text-primary), var(--color-accent));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__subtitle {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
max-width: 500px;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__platforms {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-top: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__platform {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3) var(--space-6);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
transform var(--transition-fast),
|
||||||
|
border-color var(--transition-fast),
|
||||||
|
box-shadow var(--transition-fast),
|
||||||
|
color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__platform:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: var(--shadow-glow);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.subscribe-cta__platforms {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-cta__platform {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
projects/radio-show/website/src/content.config.ts
Normal file
39
projects/radio-show/website/src/content.config.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
import { glob } from 'astro/loaders';
|
||||||
|
|
||||||
|
const episodes = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.md', base: './src/content/episodes' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
season: z.number(),
|
||||||
|
episode: z.number(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
duration: z.string(),
|
||||||
|
audioUrl: z.string().url(),
|
||||||
|
audioSize: z.number(),
|
||||||
|
episodeType: z.enum(['full', 'trailer', 'bonus']).default('full'),
|
||||||
|
originalUrl: z.string().url().optional(),
|
||||||
|
featured: z.boolean().default(false),
|
||||||
|
classic: z.boolean().default(false),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
chapters: z.array(z.object({
|
||||||
|
time: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
})).optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string().default('Mike Swanson'),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
image: z.string().optional(),
|
||||||
|
draft: z.boolean().default(false),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { episodes, blog };
|
||||||
13
projects/radio-show/website/src/content/blog/welcome-back.md
Normal file
13
projects/radio-show/website/src/content/blog/welcome-back.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
title: "The Computer Guru Show Returns"
|
||||||
|
pubDate: 2026-03-14
|
||||||
|
description: "After five years off the air, The Computer Guru Show is back with a fresh format and a brand new website."
|
||||||
|
author: "Mike Swanson"
|
||||||
|
tags: ["announcement", "show-news"]
|
||||||
|
---
|
||||||
|
|
||||||
|
After a five-year hiatus, The Computer Guru Show is officially returning to the airwaves. We have been off the air since Season 10, Episode 21 back in March 2018, but we have not been idle.
|
||||||
|
|
||||||
|
The tech landscape has changed dramatically since then. AI has gone from a buzzword to a daily tool. The way we interact with technology has fundamentally shifted. And we are here to make sense of it all -- the same way we always have: by making technology fun and simple.
|
||||||
|
|
||||||
|
Stay tuned for new episodes, and in the meantime, browse our archive of 194 classic episodes spanning Seasons 6 through 10.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "– Guest appearance on Liberty Watch"
|
||||||
|
season: 0
|
||||||
|
episode: 179
|
||||||
|
pubDate: 2014-05-23
|
||||||
|
duration: "42:34"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/specials/libertywatch5-11-14.mp3"
|
||||||
|
audioSize: 17882275
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-guest-appearance-on-liberty-watch/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Special Edition Podcast – Liberty Watch from 5/11/2014 I sat down with Charles Heller on Liberty Watch to discuss #Netneutrality This was the result.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S6E01 – Computer Guru Show – New Digs"
|
||||||
|
season: 6
|
||||||
|
episode: 1
|
||||||
|
pubDate: 2014-02-10
|
||||||
|
duration: "46:50"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/02/s6e01.mp3"
|
||||||
|
audioSize: 44963279
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s6e01-computer-guru-show-new-digs/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["net-neutrality", "streaming"]
|
||||||
|
---
|
||||||
|
|
||||||
|
\*\*\* I’m aware that we cannot hear Harry in the podcasts. I’ll see if we can get that fixed next week. \*\*\* The first episode on KVOI. Thanks for your support, everyone. Topics : Welcome to KVOI Net Neutrality Netflix vs. Verizon Legislating a Business Model
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S6E02 – Computer Guru Show – Ripples"
|
||||||
|
season: 6
|
||||||
|
episode: 2
|
||||||
|
pubDate: 2014-02-17
|
||||||
|
duration: "48:53"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/02/s6e02.mp3"
|
||||||
|
audioSize: 20536828
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s6e02-computer-guru-show-ripples/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["google", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
In this episode we talk about the ripple effects of Windows 8 and how certain vendors are responding to the new operating system. We also touch on how UFC Pay Per View is bullying sites to give up userlists. As always, your phone calls, and various other topics. Sl!cklogin : http://www.engadget.com/2014/02/16/google-acquires-slicklogin-sound-passwords/ Nike Laces : http://solecollector.com/news/tinker-hatfield-confirms-nike-mag-will-release-with-power-laces-20151/ Google Fiber \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S6E03 – Computer Guru Show – TechHole!"
|
||||||
|
season: 6
|
||||||
|
episode: 3
|
||||||
|
pubDate: 2014-02-24
|
||||||
|
duration: "48:24"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/02/s6e03.mp3"
|
||||||
|
audioSize: 46474202
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s6e03-computer-guru-show-techhole/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["apple"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Mike and Tara go over how not to be a jerk with Technology, Apple’s SSL problems, Your calls, and more! Visit the Aftershow Post for links and more details, including things we promised to update you on after the show!
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E04 – Computer Guru Show – No Topic"
|
||||||
|
season: 6
|
||||||
|
episode: 4
|
||||||
|
pubDate: 2014-03-03
|
||||||
|
duration: "48:57"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/02/s6e04.mp3"
|
||||||
|
audioSize: 46987456
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e04-computer-guru-show-no-topic/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Various Rants about Stuff 🙂
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E05 – Computer Guru Show – ID10T Governance"
|
||||||
|
season: 6
|
||||||
|
episode: 5
|
||||||
|
pubDate: 2014-03-10
|
||||||
|
duration: "47:28"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/03/s6e05.mp3"
|
||||||
|
audioSize: 45568902
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e05-computer-guru-show-id10t-governance/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Do you know what an ISP is? I hope so. Our government doesn’t.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E06 – Computer Guru Show – Double Interviews! Kihon Games and Malwarebytes"
|
||||||
|
season: 6
|
||||||
|
episode: 6
|
||||||
|
pubDate: 2014-03-22
|
||||||
|
duration: "45:15"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/03/s6e06.mp3"
|
||||||
|
audioSize: 19005582
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e06-computer-guru-show-double-interviews-kihon-games-and-malwarebytes/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["malware"]
|
||||||
|
---
|
||||||
|
|
||||||
|
For the first part of this podcast, we speak with Mark from Kihon Games about their new endeavor, “Conquest of Champions”. We also speak with Adam from Malwarebytes about what’s next for the anti-malware company, and how you should be protecting your computer.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E07 – Martian Koalas switch to Linux"
|
||||||
|
season: 6
|
||||||
|
episode: 7
|
||||||
|
pubDate: 2014-03-23
|
||||||
|
duration: "48:27"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/03/s6e07.mp3"
|
||||||
|
audioSize: 46507638
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e07-martian-koalas-switch-to-linux/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["linux", "space", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We had a lot of fun discussing the Game Developer’s Conference that finished up last week, the prospect of a Russian Space Hotel, and the end of support for Windows XP. PLUS, what you type can now be deciphered by sound…YIKES! Didn’t get to listen in live? Catch up by playing the podcast. Regarding the \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E08 – Facebook Rage, ICAN Tango II, and a brand new time!"
|
||||||
|
season: 6
|
||||||
|
episode: 8
|
||||||
|
pubDate: 2014-03-31
|
||||||
|
duration: "48:55"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/03/s6e08.mp3"
|
||||||
|
audioSize: 46969065
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e08-facebook-rage-ican-tango-ii-and-a-brand-new-time/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["facebook"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Listen in as we discuss the pros and cons of Facebook spending billions of dollars on acquiring new projects, including the Oculus Rift. We also talk with Chuck Clayton about the ICAN Tango II Charity Event (details below), and help out a few callers with their technology issues. AND – WE ARE MOVING! That’s right, \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E09 – Tracking Data"
|
||||||
|
season: 6
|
||||||
|
episode: 9
|
||||||
|
pubDate: 2014-04-07
|
||||||
|
duration: "46:36"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/04/s6e09.mp3"
|
||||||
|
audioSize: 19573397
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e09-tracking-data/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["google"]
|
||||||
|
---
|
||||||
|
|
||||||
|
New time, new board-op, new experiences. This week we bring you several stories on how tracking data is and isn’t used, and how Google is shaking things up with everything they do.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E10 – Commercial Break!"
|
||||||
|
season: 6
|
||||||
|
episode: 10
|
||||||
|
pubDate: 2014-04-13
|
||||||
|
duration: "48:59"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/04/s6e10.mp3"
|
||||||
|
audioSize: 47026744
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e10-commercial-break/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["google"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Today we talked about the Heartbleed Bug (and a list of passwords you should change \[from Mashable, by the way\]), the sale of Google Glass for one day, the IRS not sticking to a deadline, and 3D Printing for the masses brought to you by … Staples? We also have a little debate about those darned commercials. \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E11 – Technology Optimism and Fear"
|
||||||
|
season: 6
|
||||||
|
episode: 11
|
||||||
|
pubDate: 2014-04-21
|
||||||
|
duration: "48:02"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/04/s6e11.mp3"
|
||||||
|
audioSize: 46119665
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e11-technology-optimism-and-fear/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
On this episode, we talked about whether we were more optimistic or fearful of today’s technology. This was all spurred by a recent study by the Pew Research Center, which broke down what people think about technology such as wearable tech, teleportation, and driverless vehicles. While some numbers are optimistic, there is still a healthy \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E12 – The beginning of the end of “Net Neutrality”"
|
||||||
|
season: 6
|
||||||
|
episode: 12
|
||||||
|
pubDate: 2014-04-28
|
||||||
|
duration: "45:06"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/04/s6e12.mp3"
|
||||||
|
audioSize: 43292589
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e12-the-beginning-of-the-end-of-net-neutrality/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["net-neutrality"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Just as we are beginning to see the power that free resources produce, changes in the architecture of the Internet–both legal and technical–are sapping the Internet of this power. Fueled by bias in favor of control, pushed by those whose financial interest favor control, our social and political institutions are ratifying changes in the Internet \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E13 – Open Internet and Star Wars Day"
|
||||||
|
season: 6
|
||||||
|
episode: 13
|
||||||
|
pubDate: 2014-05-07
|
||||||
|
duration: "50:39"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/05/s6e13.mp3"
|
||||||
|
audioSize: 48629617
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s06e13-open-internet-and-star-wars-day/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["net-neutrality"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We get just a bit more in-depth into the issue with Net Neutrality, specifically peering, to clarify a few things that were said last week. If you’d like to let your thoughts be known to the FCC (which, you should) then you need to go to this website for information. It doesn’t matter if you’re \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E14 – Printing, and Trojans, and Net Neutrality… oh my!"
|
||||||
|
season: 6
|
||||||
|
episode: 14
|
||||||
|
pubDate: 2014-05-10
|
||||||
|
duration: "49:00"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/05/s6e14.mp3"
|
||||||
|
audioSize: 47043773
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e14-printing-and-trojans-and-net-neutrality-oh-my/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["facebook", "malware", "net-neutrality"]
|
||||||
|
---
|
||||||
|
|
||||||
|
On today’s show, we discuss a plethora of topics, including a heated discussion on Net Neutrality, turning your car into a hybrid, printing out makeup, and how we can help you with your computer REMOTELY. Links are below: Malwarebytes AntiExploit Hybrid Conversions Facebook Pokes (RIP) MINK Makeup Printer and Net Neutrality
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E15 – Get ready for summer!"
|
||||||
|
season: 6
|
||||||
|
episode: 15
|
||||||
|
pubDate: 2014-05-23
|
||||||
|
duration: "45:39"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/05/s6e15.mp3"
|
||||||
|
audioSize: 43827225
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e15-get-ready-for-summer/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
We all know that Spring Cleaning usually entails cleaning out your closet and garage, but you also need to focus on your computer. If you don’t want to lose all your information to a killer monsoon lightning strike, then you need to take some precautions. We talk about this, Russia’s threat against GPS systems, random \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06 E16 – E3 Pre Party!"
|
||||||
|
season: 6
|
||||||
|
episode: 16
|
||||||
|
pubDate: 2014-05-25
|
||||||
|
duration: "47:58"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/05/s6e16.mp3"
|
||||||
|
audioSize: 46058332
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06-e16-e3-pre-party/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Such energy. Much random. Wow. Today we talked with Clay from Nerd Junkies about our trip to E3 (Electronic Entertainment Expo) 2014, coming up in just a couple of weeks. (Links to all E3 items discussed, as well as the Nerd Junkies info, will be posted below.) We also spoke about Ebay getting hacked, the importance \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06 E17 – Snowden and Privacy"
|
||||||
|
season: 6
|
||||||
|
episode: 17
|
||||||
|
pubDate: 2014-05-31
|
||||||
|
duration: "50:39"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/05/s6e17.mp3"
|
||||||
|
audioSize: 48629617
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06-e17-snowden-and-privacy/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["privacy"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We discussed the case of Edward Snowden today, and whether or not he’s a “whistleblower” or a “traitor”. We also mentioned a documentary, “Frontline”, which asks the question whether or not we’re “safer” after the events of 9/11. Another thing that has hit the news is TrueCrypt asking its users to go elsewhere because, “WARNING: \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E18 – Hack your car?"
|
||||||
|
season: 6
|
||||||
|
episode: 18
|
||||||
|
pubDate: 2014-06-08
|
||||||
|
duration: "49:48"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/06/s6e18.mp3"
|
||||||
|
audioSize: 47814908
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e18-hack-your-car/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["android", "security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
A multitude of subjects covered this week: Car Hacking Safari plays DuckDuckGo Backup your Android with Lookout Need a link to something we covered? Send us a message!
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06 E19 – Back from E3!"
|
||||||
|
season: 6
|
||||||
|
episode: 19
|
||||||
|
pubDate: 2014-06-21
|
||||||
|
duration: "48:34"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/06/s6e19.mp3"
|
||||||
|
audioSize: 46634591
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06-e19-back-from-e3/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["tesla"]
|
||||||
|
---
|
||||||
|
|
||||||
|
So, we’re back from E3! We talk about some of the things we saw there, and what we may or may not be excited about. ALSO, Tesla has opened up their technologies to the masses. Is this wise? What does this mean with patent law? We discuss this and more! Listen below.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06 E20 – Our 20th episode on KVOI!"
|
||||||
|
season: 6
|
||||||
|
episode: 20
|
||||||
|
pubDate: 2014-06-24
|
||||||
|
duration: "49:06"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/06/s6e20.mp3"
|
||||||
|
audioSize: 47144083
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06-e20-our-20th-episode-on-kvoi/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We would like to remind you that this show is all about YOU. We don’t just talk about technology, we talk about YOUR technology. So, for our 20th episode, we took your calls and talked about what YOU wanted to talk about, which included: Solar Roadways Security Cameras Email Archiving in Outlook and other random topics! \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06 E21 – Ownphones, eSports, and YOUR calls!"
|
||||||
|
season: 6
|
||||||
|
episode: 21
|
||||||
|
pubDate: 2014-06-28
|
||||||
|
duration: ""
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/06/s6e21.mp3"
|
||||||
|
audioSize: 5242880
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06-e21-ownphones-esports-and-your-calls/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Today we had a great conversation with Itamar Jobani of Ownphones, 3D printed earbuds that are personalized to fit into YOUR ears. Check out their information below, and help them out with their Kickstarter campaign. As a special “Thank You”, the first 1000 people that help fund Ownphones will be able to purchase their own \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E22 – Carnivorous Coffee Tables"
|
||||||
|
season: 6
|
||||||
|
episode: 22
|
||||||
|
pubDate: 2014-07-10
|
||||||
|
duration: "48:56"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/07/s6e22.mp3"
|
||||||
|
audioSize: 20551639
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e22-carnivorous-coffee-tables/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
We went through a lot of topics today, discussing everything from Solid State Hard Drives to Lindsay Lohan. In fact, we had so many different topics that we touched on yesterday, we didn’t even really talk about all of them in detail. So, below you’ll find all of the links to everything you want to \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E23 – Unscrewed Theater"
|
||||||
|
season: 6
|
||||||
|
episode: 23
|
||||||
|
pubDate: 2014-07-12
|
||||||
|
duration: "47:55"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/07/s6e23.mp3"
|
||||||
|
audioSize: 46009431
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e23-unscrewed-theater/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["malware"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Today we had Mike from Unscrewed Theater on the show to promote their awesome antics, and the fact that The Guru will be joining them on July 26th as a special guest. We had a full house with four people on the show at once! We talked about the resurgence of the Zeus virus, how to \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E24 – Guru UNLEASHED!"
|
||||||
|
season: 6
|
||||||
|
episode: 24
|
||||||
|
pubDate: 2014-07-21
|
||||||
|
duration: "48:50"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/07/s6e24.mp3"
|
||||||
|
audioSize: 46883384
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e24-guru-unleashed/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
This episode we went on remote to present Guru Unleashed at the Arizona Computer Guru Shop. We had Chris from Unscrewed Theater on to talk about their group, “Not Burn Out, Just Unscrewed”, and Mike’s upcoming appearance with them on the 26th of July. It was a great experience, and fun was had by all. \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E25 – Verizon, you’re on notice"
|
||||||
|
season: 6
|
||||||
|
episode: 25
|
||||||
|
pubDate: 2014-07-29
|
||||||
|
duration: "47:34"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/07/s6e25.mp3"
|
||||||
|
audioSize: 11420529
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e25-verizon-youre-on-notice/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
We opened up the show with congratulations for “Weird” Al Yankovic for FINALLY hitting #1 on Billboard Top 200 for his album “Mandatory Fun”. We also talked about eSports and the number of sponsors that are coming out to support them. Additionally, Russia has put a bounty up on Tor, Verizon has started throttling the \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E26 – 911 emergency! Facebook is down!"
|
||||||
|
season: 6
|
||||||
|
episode: 26
|
||||||
|
pubDate: 2014-08-03
|
||||||
|
duration: "47:39"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/08/s6e26.mp3"
|
||||||
|
audioSize: 45744756
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e26-911-emergency-facebook-is-down/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["facebook"]
|
||||||
|
---
|
||||||
|
|
||||||
|
That’s it. The world is falling apart. People are calling 911 when Facebook goes down for a half an hour, you can take a picture of a key and get a physical copy printed at a kiosk, and the CIA and Senate are having a lover’s spat. On the plus side, “Idiocracy” is alive and \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S06E27 – Ding Dong, the Windows 8 is dead!"
|
||||||
|
season: 6
|
||||||
|
episode: 27
|
||||||
|
pubDate: 2014-08-10
|
||||||
|
duration: "47:49"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/08/s6e27.mp3"
|
||||||
|
audioSize: 45900655
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s06e27-ding-dong-the-windows-8-is-dead/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
It was a busy show, as we discussed topics that varied from the upcoming Windows 9, to the insecurity of USB, to the arrest of a 20 year old who created a proxy. We didn’t get to some topics, as we just didn’t have enough time to touch on them all, but we can link \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S6E28 – Encryption is Key"
|
||||||
|
season: 6
|
||||||
|
episode: 28
|
||||||
|
pubDate: 2014-08-16
|
||||||
|
duration: "48:40"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/08/s6e28.mp3"
|
||||||
|
audioSize: 46723616
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s6e28-encryption-is-key/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We had so much to talk about today, that we just couldn’t fit it all in! Most of what we had to talk about revolved around encryption, and the fact that everyone should be using it. There are too many ways to hack into an unencrypted system, so if you want your information to be \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E01 – Season Seven Unleashed!"
|
||||||
|
season: 7
|
||||||
|
episode: 1
|
||||||
|
pubDate: 2014-08-23
|
||||||
|
duration: "48:48"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/08/s7e01.mp3"
|
||||||
|
audioSize: 46847439
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e1-season-seven-unleashed/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
We are now into Season 7 of the Computer Guru Radio Show! We started things off by hosting a Guru Unleashed at Arizona Computer Guru! Though we had a little technical trouble to begin with, we got past it quickly and got to the things that matter. Whether or not you think they’re useful, here’s \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E02 – Microsoft says NO."
|
||||||
|
season: 7
|
||||||
|
episode: 2
|
||||||
|
pubDate: 2014-08-30
|
||||||
|
duration: "48:23"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/08/s7e02.mp3"
|
||||||
|
audioSize: 46452779
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e02-microsoft-says/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["microsoft"]
|
||||||
|
---
|
||||||
|
|
||||||
|
So today we started the show off talking about tablets, in a way picking up off of the conversation we had at the end of last week. We also discussed Microsoft and their decision to go against court orders, mandatory kill-switch in California, and social networks such as Pinterest! PC Sales Up / Tablets Down Microsoft \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E03 – Security, metadata, and … the #Fappening"
|
||||||
|
season: 7
|
||||||
|
episode: 3
|
||||||
|
pubDate: 2014-09-06
|
||||||
|
duration: "49:13"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/09/s7e03.mp3"
|
||||||
|
audioSize: 47253589
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e03-security-metadata-fappening/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
That’s right. We discussed the #Fappening and the nude photos of all the celebrities that came out this past week. You can’t have a security breach that HUGE and expect us not to talk about it. So what side of the debate do you fall on? Does blame lie more on the celebrities or the \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E04 – No, it’s not a phone…"
|
||||||
|
season: 7
|
||||||
|
episode: 4
|
||||||
|
pubDate: 2014-09-15
|
||||||
|
duration: "49:43"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/09/s7e04.mp3"
|
||||||
|
audioSize: 11935141
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e04-phone/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["chrome", "microsoft", "privacy", "security", "streaming"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Mike flew solo this week as he made his way through topics of privacy, entertainment on the go, and firing back at phishing schemes. He also mentioned the NoPhone, which is (indubitably) not a phone. Seriously. It’s not. HBO Go considering standalone payment Roku or Chromecast Turning the tables on “Microsoft” phishing schemes System Update \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E05 – Netflix and Canada, eh?"
|
||||||
|
season: 7
|
||||||
|
episode: 5
|
||||||
|
pubDate: 2014-09-21
|
||||||
|
duration: "49:11"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/09/s7e05.mp3"
|
||||||
|
audioSize: 47220570
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e05-netflix-canada-eh/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["streaming"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Canada is usually known for being a pretty polite nation, so what has Netflix done to ruffle their feathers so much? Can it really all boil down to greed? Canada is asking Netflix for private data on its consumers. Most people seem to agree with Netflix in saying, “no”. Mike also answers the question: What’s \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E06 – Stay the Hell away from my phone!"
|
||||||
|
season: 7
|
||||||
|
episode: 6
|
||||||
|
pubDate: 2014-09-29
|
||||||
|
duration: "49:03"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/09/s7e06.mp3"
|
||||||
|
audioSize: 47091838
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e06-stay-hell-away-phone/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
They say no news is good news, so of course we’re seeing tons of news coming out right now (most of which is depressing). We tried to start out with a little good news about the appointment of the new CTO, but the harder we try to get positive, the more the headlines bring us \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E07 – Let’s all sue Google"
|
||||||
|
season: 7
|
||||||
|
episode: 7
|
||||||
|
pubDate: 2014-10-04
|
||||||
|
duration: "49:26"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/10/s7e07.mp3"
|
||||||
|
audioSize: 11865656
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e07-lets-sue-google/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["google", "security", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We spoke about a plethora of topics today, including Tiny Death Star and Disney, Celebrities and Google, home security issues with Comcast, and even a terrible Windows 7 joke. Click the links below to find out more about the stories we brought up, comment below if you have two cents to throw in on \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E08 – C’mon, get happy!"
|
||||||
|
season: 7
|
||||||
|
episode: 8
|
||||||
|
pubDate: 2014-10-12
|
||||||
|
duration: "48:59"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/10/s7e08.mp3"
|
||||||
|
audioSize: 11758616
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e08-cmon-get-happy/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["privacy"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We’re tired of all the doom and gloom in the news, so we tried to bring some happiness and humor into the studio today. Links to all the stories are below, following a TED talk by Glenn Greewald on, “Why Privacy Matters”. Want to help keep our show going? As you may know, the show \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E09 – Learning to Fly…"
|
||||||
|
season: 7
|
||||||
|
episode: 9
|
||||||
|
pubDate: 2014-10-18
|
||||||
|
duration: "49:05"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/10/s7e09.mp3"
|
||||||
|
audioSize: 11783527
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e09/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Today we talked about Clean Energy from Lockheed Martin, Tor Routers being pulled from Kickstarter (even after being funded), a Tetris movie, and a bit about flying. We’re also still learning that we need to Encrypt EVERYTHING after Aaron’s pulls a big no-no with spyware. Additionally, if you like the show and would like to \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E10 – The cultural revolution that has spun out of control, #Gamergate"
|
||||||
|
season: 7
|
||||||
|
episode: 10
|
||||||
|
pubDate: 2014-10-25
|
||||||
|
duration: "49:26"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/10/s7e10.mp3"
|
||||||
|
audioSize: 11867327
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e09-cultural-revolution-spun-control-gamergate/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["gaming"]
|
||||||
|
---
|
||||||
|
|
||||||
|
There are a lot of views and perspectives on #Gamergate. Some support it due to issues in ethics within the gaming industry. Others oppose it, saying it’s a way for supporters to harass women. Either way you look at it, there’s really no clear understanding of the basis and endgame of #Gamergate, except that it has \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E11 – Wearable Tech, Faster DSL, and Manuel Noriega!"
|
||||||
|
season: 7
|
||||||
|
episode: 11
|
||||||
|
pubDate: 2014-11-01
|
||||||
|
duration: "49:16"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/11/s7e11.mp3"
|
||||||
|
audioSize: 11826576
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e11-wearable-tech-faster-dsl-manuel-noriega/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["apple", "google", "microsoft"]
|
||||||
|
---
|
||||||
|
|
||||||
|
In this episode, we talked a bit about Nike and Apple deepening their relationship, Microsoft’s awesome wearable tech, and DSL bumping up their service. We also discovered that Spain knows nothing about how Google works, and Manuel Noriega’s lawsuit was dismissed by the judge. If you’re interested in our GPS (Guru Protection Services) available through \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E12 – Questionable Legality and new IP"
|
||||||
|
season: 7
|
||||||
|
episode: 12
|
||||||
|
pubDate: 2014-11-10
|
||||||
|
duration: "49:17"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/11/s7e12.mp3"
|
||||||
|
audioSize: 11831696
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e12-questionable-legality-new-ip/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["google"]
|
||||||
|
---
|
||||||
|
|
||||||
|
We had a lot to discuss today, including going back to two topics we discussed last week. What has Google done about the laws brought against it? What exactly is FreedomPop? Also, the re-emergence of Popcorn Time, and the first new IP from Blizzard in 17 years! Listen to the podcast below, and check out \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E13 – What is FreedomPop?"
|
||||||
|
season: 7
|
||||||
|
episode: 13
|
||||||
|
pubDate: 2014-11-15
|
||||||
|
duration: "49:07"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/11/s7e13.mp3"
|
||||||
|
audioSize: 11789378
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e13-freedompop/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
We’re getting into gadgets! First and foremost, we talk about FreedomPop, which is something that we’ve mentioned over the past two weeks. We were able to get in touch with them, and had an awesome Q&A session with Samantha. Not only did she answer our questions, and explain what FreedomPop isall about, she also got \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E14 – DOJ and Apple butt heads"
|
||||||
|
season: 7
|
||||||
|
episode: 14
|
||||||
|
pubDate: 2014-11-25
|
||||||
|
duration: "49:04"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/11/s7e14.mp3"
|
||||||
|
audioSize: 11779660
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e14-doj-apple-butt-heads/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["apple", "iphone"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Apparently, the DoJ is still trying to get Apple to let them decrypt iPhones, because “a child will die” due to the police being unable to gain access. We also answered your phone calls, looked up “Axpergle”, and talked about some of the top video games for this holiday season. Click the links below to \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E15 – Making your computer faster"
|
||||||
|
season: 7
|
||||||
|
episode: 15
|
||||||
|
pubDate: 2014-12-08
|
||||||
|
duration: "46:54"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/12/s7e15.mp3"
|
||||||
|
audioSize: 11257838
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e15-making-computer-faster/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Maybe you didn’t want to buy a new computer over the big shopping weekend. If not, Mike discusses a few tips on how to make your current computer faster. If you did buy a new computer, we can provide you with options for recycling your old computer, as well as help protect your new computer \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E16 – Post Cyber Monday"
|
||||||
|
season: 7
|
||||||
|
episode: 16
|
||||||
|
pubDate: 2014-12-15
|
||||||
|
duration: "47:59"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/12/s7e16.mp3"
|
||||||
|
audioSize: 11519481
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e16-post-cyber-monday/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Did you go shopping online for Cyber Monday? We chatted about the biggest online retail day of the year, and what kind of deals we found. We also discussed an awesome gift list for geeks, the Hour of Code, and took some of your calls, including one rave review from one of our customers! Here’s \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E17 – Who’s responsible for the Sony hack?"
|
||||||
|
season: 7
|
||||||
|
episode: 17
|
||||||
|
pubDate: 2014-12-23
|
||||||
|
duration: "49:45"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/12/s7e17.mp3"
|
||||||
|
audioSize: 11943979
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e17-whos-responsible-sony-hack/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Sony has been hacked. ‘The Interview’ has been pulled. So has ‘Team America’. But the game ‘Glorious Leader’ lives on… Who’s responsible? North Korea? Or is there someone else… As always, thank you for listening to the show. The Computer Guru Radio Show is for you, the listener, and we love providing that service to \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E18 – Hacking, monopolies, and #WakeUpSony"
|
||||||
|
season: 7
|
||||||
|
episode: 18
|
||||||
|
pubDate: 2014-12-27
|
||||||
|
duration: "49:34"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2014/12/s7e18.mp3"
|
||||||
|
audioSize: 11900345
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s07e18-hacking-monopolies-wakeupsony/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Lots to talk about today as the year winds down to a close. The majority of our last show of 2014 was spent on discussing hacking, lulz, and Sony. There have been the issues with ‘The Interview’ and all of the leaked information, but now Sony also has the added headache of PSN being down. \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E19 – Nerd Junkies help bring in the New Year!"
|
||||||
|
season: 7
|
||||||
|
episode: 19
|
||||||
|
pubDate: 2015-01-07
|
||||||
|
duration: "47:13"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/01/s7e19.mp3"
|
||||||
|
audioSize: 11334116
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s7e19-nerd-junkies-help-bring-new-year/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["net-neutrality", "privacy"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Happy New Year! Mike brings on Clay from the Nerd Junkies to help out on the show, as Tara was out for the week (NOT snowboarding). Mike and Clay review 2014 and the great strides that technology has taken this year, not just in physical form, but also with secrecy, privacy, and net neutrality. \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "– S7E20 – All a matter of perspective"
|
||||||
|
season: 7
|
||||||
|
episode: 20
|
||||||
|
pubDate: 2015-01-11
|
||||||
|
duration: "47:38"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/01/s7e20.mp3"
|
||||||
|
audioSize: 11433695
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e20-matter-perspective/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["microsoft"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Today on the Computer Guru Show, not only is Tara back, but so are thousands of classic games! Mike gives more love to Elon Musk, Anonymous steps up for ‘Charlie Hebdo’, people all over the place are getting separation anxiety from their phones, and Microsoft is doing away with Internet Explorer. If you’d like \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E21 – Radioshack might be done, Google is our new brain, and XBox One won an Emmy… WHAT IS GOING ON?!"
|
||||||
|
season: 7
|
||||||
|
episode: 21
|
||||||
|
pubDate: 2015-01-17
|
||||||
|
duration: "46:46"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/01/s7e21.mp3"
|
||||||
|
audioSize: 11228268
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e21-radioshack-might-be-done-google-is-our-new-brain-and-xbox-one-won-an-emmy-what-is-going-on/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["gaming", "google", "microsoft", "security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
On this episode, we discuss learning to code for free, what the President has to say about encryption, and the fact that Microsoft’s XBox One actually one an Emmy. Check out the links below to learn more, or to listen to the podcast. If you’d like to take a minute and just sit right there, \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S07E22 – Dish made how many violations?!"
|
||||||
|
season: 7
|
||||||
|
episode: 22
|
||||||
|
pubDate: 2015-01-24
|
||||||
|
duration: "49:00"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/01/s7e22.mp3"
|
||||||
|
audioSize: 47048371
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/s7e22-dish-made-how-many-violations/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["google", "microsoft"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This episode, Mike and Tara discuss various projects from Google and Microsoft, as well as some upcoming technologies to include 3D printing your food! (Article link and video below.) Mike also took some of your calls, and answered your questions! To help sponsor the show, head on over to Patreon like Desert Pro Commercial Cleaning did. \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E23 – Mirror, mirror…"
|
||||||
|
season: 7
|
||||||
|
episode: 23
|
||||||
|
pubDate: 2015-01-31
|
||||||
|
duration: "47:04"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/01/s7e23.mp3"
|
||||||
|
audioSize: 11299530
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e23-mirror-mirror/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["net-neutrality"]
|
||||||
|
---
|
||||||
|
|
||||||
|
A mirror that can tell you what’s wrong with you? That’s right. It’s from Panasonic. We discussed this, the Belfie Stick, Net Neutrality, and more on today’s show. Check out the links and videos below, and find the podcast down at the bottom! Want to help out with the show and become a sponsor? Head over \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E24 – DO NOT go to the Pirate Bay… Regardless of what Anonymous says."
|
||||||
|
season: 7
|
||||||
|
episode: 24
|
||||||
|
pubDate: 2015-02-07
|
||||||
|
duration: "46:46"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/02/s7e24.mp3"
|
||||||
|
audioSize: 11228059
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e24-do-not-go-to-the-pirate-bay-regardless-of-what-anonymous-says/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
On this episode, there was a bit about Anonymous’ warnings on the resurrection of The Pirate Bay, and a lot of talk on the possibility of the internet being sourced as a public utility. We had a couple listeners call in to give their take on the situation, as well as plenty of chatter in the Live \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E25 – Technology hates humanity and wants us to know it"
|
||||||
|
season: 7
|
||||||
|
episode: 25
|
||||||
|
pubDate: 2015-02-14
|
||||||
|
duration: "45:43"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/02/s7e25.mp3"
|
||||||
|
audioSize: 10972849
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e25-technology-hates-humanity-and-wants-us-to-know-it/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Mike is incensed over a mistake that Verizon has made. We also talk about how technology hates you, and has no problem letting you know. Tara goes over a list of her top 10 classic co-op games for Valentine’s Day. Find the links to what we talked about below, and find the podcast at the \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E26 – Victory over Verizon"
|
||||||
|
season: 7
|
||||||
|
episode: 26
|
||||||
|
pubDate: 2015-02-24
|
||||||
|
duration: "45:58"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/02/s7e26.mp3"
|
||||||
|
audioSize: 11034126
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e26-victory-verizon/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
So what ever happened with Mike’s $1500 Verizon bill? We discuss this as well as Exploding Kittens, failing Kickstarter projects, and Lenovo’s Superfish. If you’d like to be a patron of the show, you can support us for as little as $1 a month! Head on over to Patreon just like Desert Pro Commercial Cleaners, \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E27 – “Equality is Poison”"
|
||||||
|
season: 7
|
||||||
|
episode: 27
|
||||||
|
pubDate: 2015-03-02
|
||||||
|
duration: "47:30"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/02/s7e27.mp3"
|
||||||
|
audioSize: 11403453
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e27-equality-is-poison/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["net-neutrality"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This week, there was a dress. Before that, Net Neutrality measures were approved by the FCC. This makes some people nervous, because they’re not exactly sure how Net Neutrality works, thanks to the disinformation being spread. Mike breaks down why this is a bipartisan issue, and explains why Net Neutrality is a good thing for \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E28 – Laid Back and Fancy Free"
|
||||||
|
season: 7
|
||||||
|
episode: 28
|
||||||
|
pubDate: 2015-03-07
|
||||||
|
duration: "46:08"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/03/s7e28.mp3"
|
||||||
|
audioSize: 44282800
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e28-laid-back-and-fancy-free/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
After the seriousness of the content we talked about last week, we decided to have a laid back show this week. We were all over the map with content ranging from cell phones to Java, and of course we took a few calls from listeners. If you’d like to keep the show going, head on \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E29 – Outsmarting your ISP"
|
||||||
|
season: 7
|
||||||
|
episode: 29
|
||||||
|
pubDate: 2015-03-14
|
||||||
|
duration: "57:52"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/03/s7e29.mp3"
|
||||||
|
audioSize: 55553947
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e29-outsmarting-the-internet/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Are you paying too much for internet? We think it’s possible. This week we discussed some of the problems that you can encounter in the confusing world of wi-fi, and how internet service providers might be selling you more broadband than you might need. If you’re experiencing slow speeds on the web, this is one \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E30- The Antivirus Episode"
|
||||||
|
season: 7
|
||||||
|
episode: 30
|
||||||
|
pubDate: 2015-03-24
|
||||||
|
duration: "45:21"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/03/s7e30.mp3"
|
||||||
|
audioSize: 43544201
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e30-the-antivirus-episode/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["malware"]
|
||||||
|
---
|
||||||
|
|
||||||
|
It’s that time of year again! Time for our annual antivirus episode where we cover every thing from the software we love/hate, to figuring out which websites put you at risk. Hint: if you have teenagers, they are probably visiting these sites! Listen in to find out how to protect your machine. If you’d like \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E31 – Security Measures in Flying"
|
||||||
|
season: 7
|
||||||
|
episode: 31
|
||||||
|
pubDate: 2015-03-28
|
||||||
|
duration: "58:42"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/03/s7e31.mp3"
|
||||||
|
audioSize: 56356011
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e31-security-measures-in-flying/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Today we discussed what we might be able to learn from the terrible Germanwings crash. Mike has some ideas for added security, and our audience called in with their opinions as well. Some options are an override by Air Traffic Control to gain access of the plane, and secondary authentication codes for access to the \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E32 – Badabing, Badaboom!"
|
||||||
|
season: 7
|
||||||
|
episode: 32
|
||||||
|
pubDate: 2015-04-04
|
||||||
|
duration: "46:09"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/04/s7e32.mp3"
|
||||||
|
audioSize: 44306560
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e32-badabing-badaboom/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Happy Easter weekend! Today we discussed Windows XP vs Windows 8, Revenge Porn, Acorns, and more. PLUS Cat finally speaks! Click the links below to find out more information on what we talked about, and play the podcast below! If you’d like to sponsor our show like Desert Commercial Cleaning, LLC and Perfection Auto Works, \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E33 – Apps, apps, and more apps!"
|
||||||
|
season: 7
|
||||||
|
episode: 33
|
||||||
|
pubDate: 2015-04-12
|
||||||
|
duration: "46:09"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/04/s7e33.mp3"
|
||||||
|
audioSize: 44306558
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e33-apps-apps-and-more-apps/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["malware"]
|
||||||
|
---
|
||||||
|
|
||||||
|
On today’s show we discussed batteries, apps, viruses, and took YOUR calls to answer some questions! Hopefully, you don’t become bored and distracted during the computery parts, and turn to some cat videos… oh wait… If YOU would like to become a sponsor of the show, head on over to Patreon to become a sponsor! \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E34 – The death of Reddit?"
|
||||||
|
season: 7
|
||||||
|
episode: 34
|
||||||
|
pubDate: 2015-04-19
|
||||||
|
duration: "45:35"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/04/s7e34.mp3"
|
||||||
|
audioSize: 43763629
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e34-the-death-of-reddit/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
We had some technical difficulties with the site today, so you may not have been able to livestream the show earlier today while it was being broadcast. But, fear not! We have resolved those issues, and you can now listen to the podcast. Today we talked about the government wasting money, all-female dev conferences, and \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E35 – While the Guru’s away…"
|
||||||
|
season: 7
|
||||||
|
episode: 35
|
||||||
|
pubDate: 2015-04-25
|
||||||
|
duration: "45:50"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/04/s7e35.mp3"
|
||||||
|
audioSize: 44006046
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e35-while-the-gurus-away/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Today we were Mike-less, so we brought in Tara, Howard, the crew, and Cat to talk about some awesome news items and take your calls! Click on the links below to check out what we were talking about on the show, and comment below with your thoughts! Also, we’ve provided a list of the 5 \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E36 – Rock, Paper, Scissors, Lizard, Spock"
|
||||||
|
season: 7
|
||||||
|
episode: 36
|
||||||
|
pubDate: 2015-05-02
|
||||||
|
duration: "45:59"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/05/s7e36.mp3"
|
||||||
|
audioSize: 44143136
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e36-rock-paper-scissors-lizard-spock/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
(Picture courtesy xSheldon on iFunny.co (http://ifunny.co/fun/LJ7tXAUw) Today things were pretty much back to normal as Mike regained control of the helm after missing last week’s show. Click the links below for more information on what we talked about during the show, and listen to the podcast at the end of the post. If you’d like \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E37 – Wearable Tech, Suicide Robots, Warp Drive Nonsense, and much more!"
|
||||||
|
season: 7
|
||||||
|
episode: 37
|
||||||
|
pubDate: 2015-05-09
|
||||||
|
duration: "45:53"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/05/s7e37.mp3"
|
||||||
|
audioSize: 44052857
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e37-wearable-tech-suicide-robots-warp-drive-nonsense-and-much-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["android", "apple", "robotics"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This week we were missing Tara, who took the week off to spend some time with her husband. Mike and Cat hold down the fort, discussing everything from smart watches to suicide robots and the nonsense of the EM drive (at least according to the crew). Segment 1 Smart watches: Apple VS Android Call: How can \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E38 – Smart Jewelry, Comcassed, GTA V Viruses"
|
||||||
|
season: 7
|
||||||
|
episode: 38
|
||||||
|
pubDate: 2015-05-16
|
||||||
|
duration: "45:49"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/05/s7e38.mp3"
|
||||||
|
audioSize: 43983894
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e38-smart-jewelry-comcassed-gta-v-viruses/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["malware"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Tara is back, but Mike is out of town! Howard and Cat help Tara hold down the fort. They discuss Smart Jewelry, a new word to describe poor customer service, viruses in GTA V for PC, and much more. Check it out! Segment 1 Comcassed: the crew’s adventures with terrible customer service led him to \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E39 – The Tech of Big Hero 6, Discussion about intelligence, and more!"
|
||||||
|
season: 7
|
||||||
|
episode: 39
|
||||||
|
pubDate: 2015-05-23
|
||||||
|
duration: "45:51"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/05/s7e39.mp3"
|
||||||
|
audioSize: 44017749
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e39-the-tech-of-big-hero-6-discussion-about-intelligence-and-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Mike’s back in action, joined by Cat and Tara. Together, they discuss the technology of Big Hero 6, how much we all dislike Comcast, and much more. Segment 1 -The technology of Big Hero 6 and how most of it is already real -Call: Popups on websites, AdBlock+ Segment 2 -Call: Tom’s desktop won’t connect \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E40 – New dictionary words, Silk Road, Youtube’s 10th anniversary, and much more!"
|
||||||
|
season: 7
|
||||||
|
episode: 40
|
||||||
|
pubDate: 2015-05-30
|
||||||
|
duration: "46:05"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/05/s7e40.mp3"
|
||||||
|
audioSize: 44241357
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e40/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["android", "google"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Many discussions of much news were afoot at the Computer Guru show today. Mike, Tara, Cat, and Ron weigh in on lots of interesting tech-related news. Segment 1 1700 new words added to the dictionary, mainly focused on internet and technology. Silk Road founder sentenced to life in prison Segment 2 Google announces Android M \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E41 – DARPA robots, HOUND demo, Solar Bike Paths, Tweets for Potholes, and much more!"
|
||||||
|
season: 7
|
||||||
|
episode: 41
|
||||||
|
pubDate: 2015-06-06
|
||||||
|
duration: "45:54"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/06/s7e41.mp3"
|
||||||
|
audioSize: 44063306
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e41-darpa-robots-hound-demo-solar-bike-paths-tweets-for-potholes-and-much-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["robotics", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Mike, Tara, and Cat talk about robots, virtual assistants, Windows 10, solar bike baths, self driving cars, and much more! Segment 1 DARPA Robotics Challenge 2015 – This year, the robots are competing to act as rescue bots. Mike wants the robots to move faster. Well Mike, competitions like this are how robots will start to \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E42 – Federal Employees hacked, DARPA robot challenge winners, Toolbar malware, and much more!"
|
||||||
|
season: 7
|
||||||
|
episode: 42
|
||||||
|
pubDate: 2015-06-15
|
||||||
|
duration: ""
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/06/s7e42.mp3"
|
||||||
|
audioSize: 5242880
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e42-federal-employees-hacked-darpa-robot-challenge-winners-toolbar-malware-and-much-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["malware", "robotics", "security", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Segment 1 OPM Hacking Investigation continues – All federal employees, past and present, are at risk of their information being stolen. Call – Dave’s sister’s Windows 7 Dell Laptop won’t connect to the interwebs. Wat do? Segment 2 Call – Tom is having printer trouble 🙁 DARPA robotics challenge winners! Also, lots of robot fails: Segment 3 \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E43 – LastPass Hacked, AT&T Fined, Twitter Pivots, and E3 news"
|
||||||
|
season: 7
|
||||||
|
episode: 43
|
||||||
|
pubDate: 2015-06-20
|
||||||
|
duration: "45:53"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/06/s7e43.mp3"
|
||||||
|
audioSize: 44054529
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e43-lastpass-hacked-att-fined-twitter-pivots-and-e3-news/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["gaming", "security", "twitter"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This week Tara is calling in from LA, where she’s attending the E3 gaming conference. She’s got the skinny on all the newest games and gaming news, and Mike and Cat have some awesome tech stories to share. Segment 1 LastPass was hacked – this is a big deal if you use it. Change your \[…\]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E44 – Baseball team gets hacked, Sharing photos online,"
|
||||||
|
season: 7
|
||||||
|
episode: 44
|
||||||
|
pubDate: 2015-06-27
|
||||||
|
duration: ""
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/06/s7e44.mp3"
|
||||||
|
audioSize: 5242880
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e44-baseball-team-gets-hacked-sharing-photos-online/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["security", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Mike, Cat and Tara discuss the latest in Tech news, including a hack in the baseball world, a new Lenovo Windows 8 computer stick, the state of sharing photos online, and much more!
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E45 – Ellen Pao Resigns from Reddit, Lizard Squad Antics, Facebook Icon Makeover, and more!"
|
||||||
|
season: 7
|
||||||
|
episode: 45
|
||||||
|
pubDate: 2015-07-14
|
||||||
|
duration: "37:51"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/07/s7e45.mp3"
|
||||||
|
audioSize: 54502989
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e45-ellen-pao-resigns-from-reddit-lizard-squad-antics-facebook-icon-makeover-and-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["facebook"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Sorry it took so long to get the podcast up this time guys and gals, the crew was moving over the weekend and didn’t have access to the interwebs to make it happen. He’s back in action now though, and here’s the latest episode of the Computer Guru show!
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E46 – Attack of the Callers, Windows 10 News, Drone Deliveries, and much more!"
|
||||||
|
season: 7
|
||||||
|
episode: 46
|
||||||
|
pubDate: 2015-07-18
|
||||||
|
duration: "55:00"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/07/s7e46.mp3"
|
||||||
|
audioSize: 52803669
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e46-attack-of-the-callers-windows-10-news-drone-deliveries-and-much-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["drones", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Attack of the callers! Mike and Tara are happy to answer the tech questions of just about everyone in Tucson on this episode of the Computer Guru show. They also talk about Windows 10, drone deliveries, and much more.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E47"
|
||||||
|
season: 7
|
||||||
|
episode: 47
|
||||||
|
pubDate: 2015-07-25
|
||||||
|
duration: "37:34"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/07/s7e47.mp3"
|
||||||
|
audioSize: 36066495
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e47/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E48 – Windows 10 Launch Date Extravaganza! Plus Don’t Starve, GPS, and more"
|
||||||
|
season: 7
|
||||||
|
episode: 48
|
||||||
|
pubDate: 2015-08-02
|
||||||
|
duration: "54:49"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/08/s7e48.mp3"
|
||||||
|
audioSize: 52623528
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e48-windows-10-launch-date-extravaganza-plus-dont-starve-gps-and-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
That’s right, folks! Windows 10 is officially a thing. You can totally get it for free, too, if you’ve already purchased Windows 7. But should you? Listen to this episode of the Computer Guru Radio Show to find out.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E49 – Verizon ditches contracts, the poor hitchhiking robot is dead, Mike answers your questions + more!"
|
||||||
|
season: 7
|
||||||
|
episode: 49
|
||||||
|
pubDate: 2015-08-08
|
||||||
|
duration: "44:19"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/www.gurushow.com/podcasts/2015/08/s7e49.mp3"
|
||||||
|
audioSize: 42539009
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e49-verizon-ditches-contracts-the-poor-hitchhiking-robot-is-dead-mike-answers-your-questions-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["robotics"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This week Mike and Tara discuss the latest tech news, which includes Verizon deciding to ditch contracts, the death of the hitchhiking robot, and more.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E50 – Samsung 16TB SSD, CIA leaks “apology” letter, Windows 10 Parental Controls, and much more!"
|
||||||
|
season: 7
|
||||||
|
episode: 50
|
||||||
|
pubDate: 2015-08-15
|
||||||
|
duration: "43:53"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/08/s7e50.mp3"
|
||||||
|
audioSize: 42134425
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e50-samsung-16tb-ssd-cia-leaks-apology-letter-windows-10-parental-controls-and-much-more/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: ["samsung", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This week, Mike, Cat and Tara talk about everything from the largest drive ever made (Samsung’s 16TB SSD), news about a CIA leak, Windows 10 Parental Control features and limitations, and more. They also take your calls and answer your questions. GuruShow post on Parental Controls Guide to setting up Parental Controls in Windows 10
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "S7E51 – Season 7 Finale at the new Computer Guru location, + tech news and your questions"
|
||||||
|
season: 7
|
||||||
|
episode: 51
|
||||||
|
pubDate: 2015-08-22
|
||||||
|
duration: "54:30"
|
||||||
|
audioUrl: "https://media.blubrry.com/gurushow/gurushow.com/podcasts/2015/08/s7e51.mp3"
|
||||||
|
audioSize: 52328556
|
||||||
|
episodeType: "full"
|
||||||
|
originalUrl: "https://www.gurushow.com/podcast-s7e51-season-7-finale-at-the-new-computer-guru-location-tech-news-and-your-questions/"
|
||||||
|
featured: false
|
||||||
|
classic: false
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
This week we celebrate not only the end of season 7 of the Computer Guru Show, but also the official opening of the new Computer Guru location in Tucson. Mike, Tara and Cat do the show on-location at our new East Side location, while discussing tech news and taking questions live from those of you \[…\]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user