summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--resources/views/home_new.blade.php15
-rw-r--r--resources/views/newtab.blade.php596
2 files changed, 608 insertions, 3 deletions
diff --git a/resources/views/home_new.blade.php b/resources/views/home_new.blade.php
index cb9a784..e032c02 100644
--- a/resources/views/home_new.blade.php
+++ b/resources/views/home_new.blade.php
@@ -5,7 +5,17 @@
<section>
<h5>August 2025</h5>
- <p>Currently I am living in Virginia. Outside of my job I have some side projects to work on, involving WebSockets, filesystem management (e.g. deduplication, redundancy, synchronization), web development, graphics, GPGPU, and more. I also play drums, sometimes piano, and try to figure out how I should best contribute positively towards the future of humanity (and beyond). </p>
+ <p>The intention of this website is to provide a means for me to disseminate information which I want to share, to serve as a data storage device and to provide references to other data stores, and perhaps to be a beacon of hope in this age of declining internet culture. Not a digital campfire, but more of </p>
+ <p>Here are some questions I have.</p>
+ <ul>
+ <li>What happened to Nintendo?</li>
+ <li>Why is Google actively destroying YouTube?</li>
+ <li>...</li>
+ </ul>
+ <p> Many people out there have been worried about these same issues, such as <a href = "https://x.com/joshwhiton/status/1987551329636544732">Josh Whiton</a>, so I won't write about it too much here.</p>
+ <p>I can only hope that this decade will be the one where we humans finally stop actively degrading functionality and user experience of all our tools, and return to our senses. We will also stop doing planned obsolescense, because businesses won't be able to get away with exploiting their customers. </p>
+ <p>One of my main hopes for the future of humanity is that we remember how to make good user interfaces and well-made tools. This comes down to caring about ourselves and each other, and not giving in to the corruption of whatever company has employed us to make whatever product or service. And I'm not only talking about software user interfaces. This degenerate pattern exists in other domains, such as comstumer service call centers, science journalism, and microwave ovens. </p>
+ <p>It is my goal with each of these writings to leave the reader with at least one little thing worthwhile to think about. </p>
<center> . . . </center>
<ul>
<li><a href = "resume/grothe_resume_20250809.pdf">Resume</a></li>
@@ -43,5 +53,4 @@
</footer>
@endsection
<script type = "module" src = "/js/main.js"></script>
-</html>
-
+</html> \ No newline at end of file
diff --git a/resources/views/newtab.blade.php b/resources/views/newtab.blade.php
new file mode 100644
index 0000000..882d1b5
--- /dev/null
+++ b/resources/views/newtab.blade.php
@@ -0,0 +1,596 @@
+@extends('template')
+
+@section('head')
+<title>New Tab Dashboard</title>
+<style>
+ body {
+ background: #1a1a1a;
+ color: #e0e0e0;
+ }
+
+ .dashboard-container {
+ max-width: 1800px;
+ margin: 0 auto;
+ padding: 20px;
+ }
+
+ .dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+ padding: 15px;
+ background: #2a2a2a;
+ border-radius: 8px;
+ }
+
+ .dashboard-header h1 {
+ font-size: 24px;
+ font-weight: 500;
+ margin: 0;
+ }
+
+ .settings-btn {
+ padding: 8px 16px;
+ background: #404040;
+ border: none;
+ border-radius: 5px;
+ color: #e0e0e0;
+ cursor: pointer;
+ font-size: 14px;
+ }
+
+ .settings-btn:hover {
+ background: #505050;
+ }
+
+ .grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
+ gap: 20px;
+ margin-bottom: 20px;
+ }
+
+ .widget {
+ background: #2a2a2a;
+ border-radius: 8px;
+ padding: 20px;
+ min-height: 400px;
+ max-height: 600px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .widget-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #404040;
+ }
+
+ .widget-header h2 {
+ font-size: 18px;
+ font-weight: 500;
+ margin: 0;
+ }
+
+ .widget-content {
+ flex: 1;
+ overflow-y: auto;
+ }
+
+ .widget-content::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ .widget-content::-webkit-scrollbar-track {
+ background: #1a1a1a;
+ border-radius: 4px;
+ }
+
+ .widget-content::-webkit-scrollbar-thumb {
+ background: #404040;
+ border-radius: 4px;
+ }
+
+ .widget-content::-webkit-scrollbar-thumb:hover {
+ background: #505050;
+ }
+
+ .feed-item {
+ padding: 12px;
+ margin-bottom: 10px;
+ background: #333;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background 0.2s;
+ }
+
+ .feed-item:hover {
+ background: #404040;
+ }
+
+ .feed-item-title {
+ font-size: 14px;
+ margin-bottom: 5px;
+ color: #e0e0e0;
+ line-height: 1.4;
+ }
+
+ .feed-item-meta {
+ font-size: 12px;
+ color: #999;
+ }
+
+ .loading {
+ text-align: center;
+ padding: 40px;
+ color: #999;
+ }
+
+ .error {
+ text-align: center;
+ padding: 40px;
+ color: #ff6b6b;
+ }
+
+ .refresh-btn {
+ padding: 4px 12px;
+ background: transparent;
+ border: 1px solid #404040;
+ border-radius: 4px;
+ color: #999;
+ cursor: pointer;
+ font-size: 12px;
+ }
+
+ .refresh-btn:hover {
+ background: #404040;
+ color: #e0e0e0;
+ }
+
+ .modal {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.8);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .modal.active {
+ display: flex;
+ }
+
+ .modal-content {
+ background: #2a2a2a;
+ padding: 30px;
+ border-radius: 8px;
+ max-width: 600px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+ }
+
+ .modal-header {
+ margin-bottom: 20px;
+ }
+
+ .modal-header h2 {
+ font-size: 20px;
+ margin: 0;
+ }
+
+ .form-group {
+ margin-bottom: 20px;
+ }
+
+ .form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: #999;
+ font-size: 14px;
+ }
+
+ .form-group input,
+ .form-group textarea {
+ width: 100%;
+ padding: 10px;
+ background: #1a1a1a;
+ border: 1px solid #404040;
+ border-radius: 4px;
+ color: #e0e0e0;
+ font-size: 14px;
+ }
+
+ .form-group textarea {
+ resize: vertical;
+ min-height: 100px;
+ }
+
+ .form-group small {
+ color: #666;
+ display: block;
+ margin-top: 5px;
+ font-size: 12px;
+ }
+
+ .form-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ }
+
+ .btn-primary {
+ background: #4CAF50;
+ color: white;
+ }
+
+ .btn-primary:hover {
+ background: #45a049;
+ }
+
+ .btn-secondary {
+ background: #404040;
+ color: #e0e0e0;
+ }
+
+ .btn-secondary:hover {
+ background: #505050;
+ }
+
+ @media (max-width: 768px) {
+ .grid {
+ grid-template-columns: 1fr;
+ }
+ }
+</style>
+@endsection
+
+@section('body')
+<div class="dashboard-container">
+ <div class="dashboard-header">
+ <h1>Dashboard</h1>
+ <button class="settings-btn" onclick="openSettings()">Settings</button>
+ </div>
+
+ <div class="grid">
+ <!-- YouTube Watch Later -->
+ <div class="widget">
+ <div class="widget-header">
+ <h2>YouTube Watch Later</h2>
+ <button class="refresh-btn" onclick="refreshYouTubeWatchLater()">↻</button>
+ </div>
+ <div class="widget-content" id="youtube-watch-later">
+ <div class="loading">Configure your YouTube playlist ID in settings</div>
+ </div>
+ </div>
+
+ <!-- YouTube Subscriptions -->
+ <div class="widget">
+ <div class="widget-header">
+ <h2>YouTube Subscriptions</h2>
+ <button class="refresh-btn" onclick="refreshYouTubeSubscriptions()">↻</button>
+ </div>
+ <div class="widget-content" id="youtube-subscriptions">
+ <div class="loading">Loading subscriptions...</div>
+ </div>
+ </div>
+
+ <!-- Hacker News -->
+ <div class="widget">
+ <div class="widget-header">
+ <h2>Hacker News</h2>
+ <button class="refresh-btn" onclick="refreshHackerNews()">↻</button>
+ </div>
+ <div class="widget-content" id="hackernews-feed">
+ <div class="loading">Loading Hacker News...</div>
+ </div>
+ </div>
+
+ <!-- X (Twitter) Feed -->
+ <div class="widget">
+ <div class="widget-header">
+ <h2>X Feed</h2>
+ <button class="refresh-btn" onclick="refreshTwitter()">↻</button>
+ </div>
+ <div class="widget-content" id="twitter-feed">
+ <div class="loading">Configure Twitter accounts in settings</div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<!-- Settings Modal -->
+<div class="modal" id="settings-modal">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h2>Settings</h2>
+ </div>
+ <form id="settings-form">
+ <div class="form-group">
+ <label>YouTube Watch Later Playlist ID</label>
+ <input type="text" id="youtube-playlist-id" placeholder="e.g., WL or PLxxxxxxxxx">
+ <small>
+ Note: Watch Later (WL) requires OAuth. Use a public playlist ID instead.
+ </small>
+ </div>
+
+ <div class="form-group">
+ <label>YouTube API Key</label>
+ <input type="text" id="youtube-api-key" placeholder="Your YouTube Data API v3 key">
+ </div>
+
+ <div class="form-group">
+ <label>YouTube Channel IDs (comma-separated)</label>
+ <textarea id="youtube-channels" placeholder="UCxxxxxx, UCyyyyyy, UCzzzzzz"></textarea>
+ </div>
+
+ <div class="form-group">
+ <label>X (Twitter) Usernames (comma-separated)</label>
+ <textarea id="twitter-accounts" placeholder="elonmusk, jack, naval"></textarea>
+ </div>
+
+ <div class="form-actions">
+ <button type="button" class="btn btn-secondary" onclick="closeSettings()">Cancel</button>
+ <button type="submit" class="btn btn-primary">Save</button>
+ </div>
+ </form>
+ </div>
+</div>
+
+<script>
+ // Configuration management
+ const CONFIG_KEY = 'newtab-config';
+
+ function getConfig() {
+ const config = localStorage.getItem(CONFIG_KEY);
+ return config ? JSON.parse(config) : {
+ youtubePlaylistId: '',
+ youtubeApiKey: '',
+ youtubeChannels: [],
+ twitterAccounts: []
+ };
+ }
+
+ function saveConfig(config) {
+ localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
+ }
+
+ // Settings modal
+ function openSettings() {
+ const config = getConfig();
+ document.getElementById('youtube-playlist-id').value = config.youtubePlaylistId || '';
+ document.getElementById('youtube-api-key').value = config.youtubeApiKey || '';
+ document.getElementById('youtube-channels').value = config.youtubeChannels.join(', ');
+ document.getElementById('twitter-accounts').value = config.twitterAccounts.join(', ');
+ document.getElementById('settings-modal').classList.add('active');
+ }
+
+ function closeSettings() {
+ document.getElementById('settings-modal').classList.remove('active');
+ }
+
+ document.getElementById('settings-form').addEventListener('submit', function(e) {
+ e.preventDefault();
+
+ const config = {
+ youtubePlaylistId: document.getElementById('youtube-playlist-id').value.trim(),
+ youtubeApiKey: document.getElementById('youtube-api-key').value.trim(),
+ youtubeChannels: document.getElementById('youtube-channels').value
+ .split(',')
+ .map(c => c.trim())
+ .filter(c => c),
+ twitterAccounts: document.getElementById('twitter-accounts').value
+ .split(',')
+ .map(a => a.trim())
+ .filter(a => a)
+ };
+
+ saveConfig(config);
+ closeSettings();
+ initializeFeeds();
+ });
+
+ // Close modal when clicking outside
+ document.getElementById('settings-modal').addEventListener('click', function(e) {
+ if (e.target === this) {
+ closeSettings();
+ }
+ });
+
+ // YouTube Watch Later
+ async function refreshYouTubeWatchLater() {
+ const container = document.getElementById('youtube-watch-later');
+ const config = getConfig();
+
+ if (!config.youtubeApiKey || !config.youtubePlaylistId) {
+ container.innerHTML = '<div class="error">Please configure YouTube API key and Playlist ID in settings</div>';
+ return;
+ }
+
+ container.innerHTML = '<div class="loading">Loading...</div>';
+
+ try {
+ const response = await fetch(
+ `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=20&playlistId=${config.youtubePlaylistId}&key=${config.youtubeApiKey}`
+ );
+ const data = await response.json();
+
+ if (data.error) {
+ throw new Error(data.error.message);
+ }
+
+ if (!data.items || data.items.length === 0) {
+ container.innerHTML = '<div class="error">No videos found</div>';
+ return;
+ }
+
+ container.innerHTML = data.items.map(item => `
+ <div class="feed-item" onclick="window.open('https://youtube.com/watch?v=${item.snippet.resourceId.videoId}', '_blank')">
+ <div class="feed-item-title">${escapeHtml(item.snippet.title)}</div>
+ <div class="feed-item-meta">${escapeHtml(item.snippet.channelTitle)}</div>
+ </div>
+ `).join('');
+ } catch (error) {
+ container.innerHTML = `<div class="error">Error: ${escapeHtml(error.message)}</div>`;
+ }
+ }
+
+ // YouTube Subscriptions
+ async function refreshYouTubeSubscriptions() {
+ const container = document.getElementById('youtube-subscriptions');
+ const config = getConfig();
+
+ if (!config.youtubeApiKey || config.youtubeChannels.length === 0) {
+ container.innerHTML = '<div class="error">Please configure YouTube API key and channel IDs in settings</div>';
+ return;
+ }
+
+ container.innerHTML = '<div class="loading">Loading...</div>';
+
+ try {
+ const videos = [];
+
+ for (const channelId of config.youtubeChannels) {
+ const response = await fetch(
+ `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${channelId}&maxResults=5&order=date&type=video&key=${config.youtubeApiKey}`
+ );
+ const data = await response.json();
+
+ if (data.error) {
+ throw new Error(data.error.message);
+ }
+
+ if (data.items) {
+ videos.push(...data.items.map(item => ({
+ ...item,
+ publishedAt: new Date(item.snippet.publishedAt)
+ })));
+ }
+ }
+
+ videos.sort((a, b) => b.publishedAt - a.publishedAt);
+
+ container.innerHTML = videos.slice(0, 20).map(item => `
+ <div class="feed-item" onclick="window.open('https://youtube.com/watch?v=${item.id.videoId}', '_blank')">
+ <div class="feed-item-title">${escapeHtml(item.snippet.title)}</div>
+ <div class="feed-item-meta">${escapeHtml(item.snippet.channelTitle)} • ${formatDate(item.publishedAt)}</div>
+ </div>
+ `).join('');
+ } catch (error) {
+ container.innerHTML = `<div class="error">Error: ${escapeHtml(error.message)}</div>`;
+ }
+ }
+
+ // Hacker News
+ async function refreshHackerNews() {
+ const container = document.getElementById('hackernews-feed');
+ container.innerHTML = '<div class="loading">Loading...</div>';
+
+ try {
+ const response = await fetch('https://hacker-news.firebaseio.com/v0/topstories.json');
+ const storyIds = await response.json();
+
+ const stories = await Promise.all(
+ storyIds.slice(0, 30).map(async id => {
+ const res = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);
+ return res.json();
+ })
+ );
+
+ container.innerHTML = stories.map(story => `
+ <div class="feed-item" onclick="window.open('${story.url || `https://news.ycombinator.com/item?id=${story.id}`}', '_blank')">
+ <div class="feed-item-title">${escapeHtml(story.title)}</div>
+ <div class="feed-item-meta">
+ ${story.score} points • ${story.descendants || 0} comments • ${escapeHtml(story.by)}
+ </div>
+ </div>
+ `).join('');
+ } catch (error) {
+ container.innerHTML = `<div class="error">Error loading Hacker News: ${escapeHtml(error.message)}</div>`;
+ }
+ }
+
+ // Twitter/X Feed
+ function refreshTwitter() {
+ const container = document.getElementById('twitter-feed');
+ const config = getConfig();
+
+ if (config.twitterAccounts.length === 0) {
+ container.innerHTML = '<div class="error">Please configure Twitter accounts in settings</div>';
+ return;
+ }
+
+ container.innerHTML = `
+ <div style="padding: 10px; color: #999; font-size: 14px; margin-bottom: 10px;">
+ Following: ${config.twitterAccounts.map(a => escapeHtml(a)).join(', ')}
+ </div>
+ <div style="color: #999; padding: 20px; text-align: center;">
+ Twitter embed requires direct integration.<br><br>
+ Alternative options:<br>
+ 1. Use <a href="https://nitter.net" target="_blank" style="color: #4CAF50;">Nitter</a> (privacy-friendly Twitter frontend)<br>
+ 2. Use Twitter's official embed widget (requires loading external script)<br>
+ 3. Build backend proxy to fetch tweets via Twitter API
+ </div>
+ <div style="margin-top: 15px;">
+ ${config.twitterAccounts.map(account => `
+ <div class="feed-item" onclick="window.open('https://twitter.com/${escapeHtml(account)}', '_blank')">
+ <div class="feed-item-title">@${escapeHtml(account)}</div>
+ <div class="feed-item-meta">Click to view profile</div>
+ </div>
+ `).join('')}
+ </div>
+ `;
+ }
+
+ // Utility functions
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ function formatDate(date) {
+ const now = new Date();
+ const diffMs = now - date;
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+ return date.toLocaleDateString();
+ }
+
+ // Initialize feeds on load
+ function initializeFeeds() {
+ refreshYouTubeWatchLater();
+ refreshYouTubeSubscriptions();
+ refreshHackerNews();
+ refreshTwitter();
+ }
+
+ // Auto-refresh every 5 minutes
+ setInterval(initializeFeeds, 300000);
+
+ // Initialize on page load
+ initializeFeeds();
+</script>
+@endsection