mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
start implementing combined resource form, details, draft, template, and carousel implementation, need to do lesson impl next
This commit is contained in:
parent
789356a5d6
commit
293d6d2eeb
126
lesson-1.md
Normal file
126
lesson-1.md
Normal file
@ -0,0 +1,126 @@
|
||||
# Welcome to PlebDevs Starter Course
|
||||
|
||||
<!-- <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs.com/api/get-video-url?videoKey=starter-lesson-0.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div> -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs-bucket.nyc3.cdn.digitaloceanspaces.com/starter-lesson-0.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div>
|
||||
|
||||
## Course Mission
|
||||
Welcome to the PlebDevs starter course! I'm Austin, the founder of PlebDevs, and I'm here to guide you through your journey from complete beginner to capable developer. This course is specifically designed for those new to coding, though we have plenty of intermediate and advanced content available on the platform for when you're ready to level up.
|
||||
|
||||
## Course Goals
|
||||
|
||||
### Overall PlebDevs Goals
|
||||
1. Learn how to code
|
||||
2. Build Bitcoin/Lightning/Nostr applications
|
||||
3. Help you become a developer
|
||||
|
||||
### Starter Course Objectives
|
||||
1. Provide an easy-to-follow overview of the developer journey
|
||||
2. Get you comfortable in a development environment
|
||||
3. Give you hands-on experience with core programming languages
|
||||
|
||||
## What is a PlebDev?
|
||||
|
||||
### Origins and Philosophy
|
||||
The term "PlebDev" was created about three years ago to describe a unique approach to learning development in the Bitcoin space. It represents:
|
||||
|
||||
- **Inclusive Learning**: Anyone can become a developer, regardless of background
|
||||
- **Growth Mindset**: Embracing the journey from beginner to professional
|
||||
- **Practical Focus**: Emphasizing real-world application development
|
||||
- **Community Support**: Learning and growing together
|
||||
|
||||
### Key Characteristics
|
||||
- 🌱 **Growth-Focused**: PlebDevs are always learning and improving
|
||||
- 🎯 **App-Centric**: Focus on building applications rather than protocol development
|
||||
- 🆕 **Embrace Being New**: Being a new developer is infinitely better than not coding at all
|
||||
- 🤝 **Community-Driven**: Bitcoin/Lightning/Nostr ecosystem needs more developers like you!
|
||||
|
||||
## Our Learning Approach
|
||||
|
||||
### Core Principles
|
||||
1. **Lower Barriers**
|
||||
- Simplify complex concepts
|
||||
- Focus on practical understanding
|
||||
- Build confidence through action
|
||||
|
||||
2. **Project-Based Learning**
|
||||
- Learn by doing
|
||||
- Create real applications
|
||||
- Build a portfolio as you learn
|
||||
|
||||
3. **MVP (Minimum Viable Product) Focus**
|
||||
- Start with core functionality
|
||||
- Get things working first
|
||||
- Iterate and improve
|
||||
|
||||
4. **Actionable Knowledge**
|
||||
- Focus on the 20% that delivers 80% of results
|
||||
- Learn what you can use right away
|
||||
- Build practical skills
|
||||
|
||||
### Teaching Methods
|
||||
- Detailed concept breakdowns
|
||||
- Line-by-line code explanations
|
||||
- Interactive learning
|
||||
- 1:1 support available
|
||||
- Community-driven progress
|
||||
|
||||
## Course Structure
|
||||
|
||||
### The Learning Path
|
||||
Instead of the traditional bottom-up approach, we use a project-focused method:
|
||||
```
|
||||
🏔️ Advanced Skills
|
||||
🏔️ Projects & Practice
|
||||
🏔️ Core Concepts
|
||||
🏔️ Development Environment
|
||||
🏔️ Getting Started
|
||||
```
|
||||
|
||||
We'll create checkpoints through projects, allowing you to:
|
||||
- Verify your understanding
|
||||
- Build your portfolio
|
||||
- See real progress
|
||||
- Have reference points for review
|
||||
|
||||
## Student Expectations
|
||||
|
||||
### What We Expect From You
|
||||
- **High Agency**: Take ownership of your learning journey
|
||||
- **Active Participation**: Engage with the material and community
|
||||
- **Persistence**: Push through challenges
|
||||
- **Curiosity**: Ask questions and explore concepts
|
||||
|
||||
### What You Can Expect From Us
|
||||
- Clear, practical instruction
|
||||
- Comprehensive support
|
||||
- Real-world applications
|
||||
- Community backing
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Next Steps
|
||||
1. Ensure you're ready to commit to learning
|
||||
2. Set up your development environment (next lesson)
|
||||
3. Join our community
|
||||
4. Start building!
|
||||
|
||||
## Resources and Support
|
||||
|
||||
### Where to Get Help
|
||||
- Course Discussion Forums
|
||||
- PlebDevs Discord Community
|
||||
- 1:1 Mentoring Options
|
||||
- Course Project Repositories
|
||||
|
||||
### Tips for Success
|
||||
1. Code daily, even if just for 30 minutes
|
||||
2. Focus on understanding rather than memorizing
|
||||
3. Build projects that interest you
|
||||
4. Engage with the community
|
||||
5. Don't be afraid to ask questions
|
||||
|
||||
## Remember
|
||||
You don't need to become a "10x developer" overnight. The goal is to start writing code, build useful things, and gradually improve. Every expert was once a beginner, and the journey of a thousand miles begins with a single line of code.
|
||||
|
||||
Ready to begin? Let's dive into the next lesson where we'll set up your development environment! 🚀
|
173
lesson-2.md
Normal file
173
lesson-2.md
Normal file
@ -0,0 +1,173 @@
|
||||
# Setting Up Your Code Editor
|
||||
|
||||
<!-- <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs.com/api/get-video-url?videoKey=starter-lesson-1.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div> -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs-bucket.nyc3.cdn.digitaloceanspaces.com/starter-lesson-1.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div>
|
||||
|
||||
## Introduction
|
||||
In this lesson, we'll set up the most fundamental tool in your development journey: your code editor. This is where you'll spend most of your time writing, testing, and debugging code, so it's crucial to get comfortable with it from the start.
|
||||
|
||||
## What is an IDE?
|
||||
|
||||
### Definition
|
||||
An IDE (Integrated Development Environment) is a software application that provides comprehensive facilities for software development. Think of it as your complete workshop for writing code.
|
||||
|
||||
### Key Components
|
||||
1. **Code Editor**
|
||||
- Where you write and edit code
|
||||
- Provides syntax highlighting
|
||||
- Helps with code formatting
|
||||
- Makes code easier to read and write
|
||||
|
||||
2. **Compiler/Interpreter**
|
||||
- Runs your code
|
||||
- Translates your code into executable instructions
|
||||
- Helps test your applications
|
||||
|
||||
3. **Debugging Tools**
|
||||
- Help find and fix errors
|
||||
- Provide error messages and suggestions
|
||||
- Make problem-solving easier
|
||||
|
||||
## Setting Up Visual Studio Code
|
||||
|
||||
### Why VS Code?
|
||||
- Free and open-source
|
||||
- Lightweight yet powerful
|
||||
- Excellent community support
|
||||
- Popular among developers
|
||||
- Great for beginners and experts alike
|
||||
|
||||
### Installation Steps
|
||||
1. Visit [code.visualstudio.com](https://code.visualstudio.com)
|
||||
2. Download the version for your operating system
|
||||
3. Run the installer
|
||||
4. Follow the installation prompts
|
||||
|
||||
### Essential VS Code Features
|
||||
|
||||
#### 1. Interface Navigation
|
||||
- **File Explorer** (Ctrl/Cmd + Shift + E)
|
||||
- Browse and manage your files
|
||||
- Create new files and folders
|
||||
- Navigate your project structure
|
||||
|
||||
- **Search** (Ctrl/Cmd + Shift + F)
|
||||
- Find text across all files
|
||||
- Replace text globally
|
||||
- Search with regular expressions
|
||||
|
||||
- **Source Control** (Ctrl/Cmd + Shift + G)
|
||||
- Track changes in your code
|
||||
- Commit and manage versions
|
||||
- Integrate with Git
|
||||
|
||||
#### 2. Terminal Integration
|
||||
To open the integrated terminal:
|
||||
- Use ``` Ctrl + ` ``` (backtick)
|
||||
- Or View → Terminal from the menu
|
||||
- Basic terminal commands:
|
||||
```bash
|
||||
ls # List files (dir on Windows)
|
||||
cd # Change directory
|
||||
clear # Clear terminal
|
||||
code . # Open VS Code in current directory
|
||||
```
|
||||
|
||||
#### 3. Essential Extensions
|
||||
Install these extensions to enhance your development experience:
|
||||
1. **ESLint**
|
||||
- Helps find and fix code problems
|
||||
- Enforces coding standards
|
||||
- Improves code quality
|
||||
|
||||
2. **Prettier**
|
||||
- Automatically formats your code
|
||||
- Maintains consistent style
|
||||
- Saves time on formatting
|
||||
|
||||
3. **Live Server**
|
||||
- Runs your web pages locally
|
||||
- Auto-refreshes on save
|
||||
- Great for web development
|
||||
|
||||
### Important Keyboard Shortcuts
|
||||
```
|
||||
Ctrl/Cmd + S # Save file
|
||||
Ctrl/Cmd + C # Copy
|
||||
Ctrl/Cmd + V # Paste
|
||||
Ctrl/Cmd + Z # Undo
|
||||
Ctrl/Cmd + Shift + P # Command palette
|
||||
Ctrl/Cmd + P # Quick file open
|
||||
```
|
||||
|
||||
## Writing Your First Code
|
||||
Let's create and run a simple HTML file:
|
||||
|
||||
1. Create a new file (`index.html`)
|
||||
2. Add basic HTML content:
|
||||
```html
|
||||
<h1>Hello World!</h1>
|
||||
```
|
||||
3. Save the file (Ctrl/Cmd + S)
|
||||
4. Open in browser or use Live Server
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. File Organization
|
||||
- Keep related files together
|
||||
- Use clear, descriptive names
|
||||
- Create separate folders for different projects
|
||||
|
||||
### 2. Regular Saving
|
||||
- Save frequently (Ctrl/Cmd + S)
|
||||
- Watch for the unsaved dot indicator
|
||||
- Enable auto-save if preferred
|
||||
|
||||
### 3. Terminal Usage
|
||||
- Get comfortable with basic commands
|
||||
- Use the integrated terminal
|
||||
- Practice navigation and file operations
|
||||
|
||||
## Troubleshooting Common Issues
|
||||
|
||||
### 1. Installation Problems
|
||||
- Ensure you have admin rights
|
||||
- Check system requirements
|
||||
- Use official download sources
|
||||
|
||||
### 2. Extension Issues
|
||||
- Keep extensions updated
|
||||
- Disable conflicting extensions
|
||||
- Restart VS Code after installation
|
||||
|
||||
### 3. Performance
|
||||
- Don't install too many extensions
|
||||
- Regular restart of VS Code
|
||||
- Keep your system updated
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Practice Navigation**
|
||||
- Create and manage files
|
||||
- Use the integrated terminal
|
||||
- Try keyboard shortcuts
|
||||
|
||||
2. **Customize Your Editor**
|
||||
- Explore themes
|
||||
- Adjust font size
|
||||
- Configure auto-save
|
||||
|
||||
3. **Prepare for Next Lesson**
|
||||
- Keep VS Code open
|
||||
- Get comfortable with the interface
|
||||
- Practice basic operations
|
||||
|
||||
## Additional Resources
|
||||
- [VS Code Documentation](https://code.visualstudio.com/docs)
|
||||
- [Keyboard Shortcuts Reference](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)
|
||||
- [VS Code Tips and Tricks](https://code.visualstudio.com/docs/getstarted/tips-and-tricks)
|
||||
|
||||
Remember: Your code editor is your primary tool as a developer. Take time to get comfortable with it, and don't worry about mastering everything at once. Focus on the basics we covered in the video, and you'll naturally learn more features as you need them.
|
||||
|
||||
Happy coding! 🚀
|
175
lesson-3.md
Normal file
175
lesson-3.md
Normal file
@ -0,0 +1,175 @@
|
||||
# Setting Up Git and GitHub: A Developer's Foundation
|
||||
|
||||
<!-- <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs.com/api/get-video-url?videoKey=starter-lesson-2.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div> -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs-bucket.nyc3.cdn.digitaloceanspaces.com/starter-lesson-2.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div>
|
||||
|
||||
## Lesson Overview
|
||||
In this lesson, we'll establish one of the most important foundations of your development journey: version control with Git and GitHub. This knowledge will enable you to track your code, back it up in the cloud, and start building your developer portfolio.
|
||||
|
||||
## Prerequisites
|
||||
- Visual Studio Code installed
|
||||
- Terminal/Command Line basics
|
||||
- GitHub account (we'll create one in this lesson)
|
||||
|
||||
## Key Learning Objectives
|
||||
- Understand what Git and GitHub are and why they're essential
|
||||
- Set up Git locally and connect it to GitHub
|
||||
- Learn basic Git commands and workflow
|
||||
- Create your first repository and commit
|
||||
- Establish good Git habits for your developer journey
|
||||
|
||||
## What is Git and GitHub?
|
||||
|
||||
### Git: Your Local Version Control
|
||||
- A version control system that tracks code changes over time
|
||||
- Prevents accidental overwrites of your work
|
||||
- Enables multiple developers to work on the same project safely
|
||||
- Runs locally on your machine
|
||||
|
||||
### GitHub: Your Code in the Cloud
|
||||
- A web-based platform that extends Git
|
||||
- Cloud storage for your code repositories
|
||||
- Enables code sharing and collaboration
|
||||
- Includes features like:
|
||||
- Issue tracking
|
||||
- Pull requests
|
||||
- Project management tools
|
||||
- Code review capabilities
|
||||
|
||||
## Why Use GitHub?
|
||||
|
||||
### 1. Portfolio Building
|
||||
- Acts as your "proof of work" as a developer
|
||||
- Shows your coding activity through contribution graphs
|
||||
- Demonstrates your consistency and dedication
|
||||
- Serves as a public showcase of your projects
|
||||
|
||||
### 2. Collaboration and Learning
|
||||
- Access millions of open-source projects
|
||||
- Learn from other developers' code
|
||||
- Contribute to real-world projects
|
||||
- Get feedback on your code
|
||||
- Work effectively in teams
|
||||
|
||||
### 3. Code Safety and Access
|
||||
- All your code is safely stored in the cloud
|
||||
- Access your projects from anywhere
|
||||
- Never lose your work due to computer issues
|
||||
|
||||
## Essential GitHub Terminology
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| Repository (Repo) | A folder containing your project files and version history |
|
||||
| Commit | A saved change or addition to your code |
|
||||
| Staging | Marking changes to be included in your next commit |
|
||||
| Push | Sending your local commits to GitHub |
|
||||
| Branch | A separate version of your code for new features or experiments |
|
||||
| Pull Request (PR) | A request to merge changes from one branch to another |
|
||||
| Clone | Creating a local copy of a remote repository |
|
||||
| Fork | Creating your own copy of someone else's repository |
|
||||
|
||||
## Hands-on Practice
|
||||
|
||||
### Setting Up Git
|
||||
1. Install Git from https://git-scm.com/downloads
|
||||
2. Configure your identity:
|
||||
```bash
|
||||
git config --global user.name "Your Name"
|
||||
git config --global user.email "your.email@example.com"
|
||||
```
|
||||
|
||||
### Your First Repository
|
||||
1. Create a new repository on GitHub named "hello-world"
|
||||
2. Initialize Git locally:
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "My first commit"
|
||||
git remote add origin <your-repository-url>
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
## Basic Git Workflow Quick Reference
|
||||
|
||||
### Pushing Code to GitHub
|
||||
```bash
|
||||
# 1. Stage your changes
|
||||
git add .
|
||||
|
||||
# 2. Commit your changes with a message
|
||||
git commit -m "Describe your changes here"
|
||||
|
||||
# 3. Push to GitHub
|
||||
git push
|
||||
```
|
||||
|
||||
### Getting Code from GitHub
|
||||
```bash
|
||||
# If you already have the repository locally:
|
||||
git pull
|
||||
|
||||
# If you need to download a repository:
|
||||
git clone https://github.com/username/repository.git
|
||||
```
|
||||
|
||||
## Building Good Habits
|
||||
|
||||
### Daily Git Practice
|
||||
- Make it a goal to push code every day
|
||||
- Even small changes count
|
||||
- Use your GitHub contribution graph as motivation
|
||||
- Track your progress over time
|
||||
|
||||
### Best Practices
|
||||
1. Commit often with clear messages
|
||||
2. Pull before you start working
|
||||
3. Push your changes when you finish
|
||||
4. Keep each project in its own repository
|
||||
5. Include README files to explain your projects
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### "No upstream branch" Error
|
||||
If you see this error when pushing:
|
||||
```bash
|
||||
git push --set-upstream origin main
|
||||
```
|
||||
|
||||
### Changes Not Showing Up
|
||||
1. Check if changes are staged:
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
2. Make sure you've committed:
|
||||
```bash
|
||||
git commit -m "Your message"
|
||||
```
|
||||
3. Verify you've pushed:
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
## Exercise: Start Your Journey
|
||||
1. Create your GitHub account if you haven't already
|
||||
2. Set up Git locally using the commands we covered
|
||||
3. Create your first repository named "hello-world"
|
||||
4. Make your first commit
|
||||
5. Push your code to GitHub
|
||||
6. Make a habit of pushing code daily
|
||||
|
||||
## Additional Resources
|
||||
- [GitHub Documentation](https://docs.github.com)
|
||||
- [Git Documentation](https://git-scm.com/doc)
|
||||
- Practice with [GitHub Learning Lab](https://lab.github.com)
|
||||
|
||||
## Next Steps
|
||||
- Start tracking all your code projects with Git
|
||||
- Begin building your portfolio on GitHub
|
||||
- Join the open-source community
|
||||
- Collaborate with other developers
|
||||
|
||||
Remember: Every developer started where you are now. The key is consistency and persistence. Make pushing code to GitHub a daily habit, and you'll be amazed at your progress over time.
|
||||
|
||||
Happy coding! 🚀
|
255
lesson-4.md
Normal file
255
lesson-4.md
Normal file
@ -0,0 +1,255 @@
|
||||
# Introduction to HTML: Building Your First Webpage
|
||||
|
||||
<!-- <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs.com/api/get-video-url?videoKey=starter-lesson-4.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div> -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs-bucket.nyc3.cdn.digitaloceanspaces.com/starter-lesson-3.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div>
|
||||
|
||||
## What is HTML?
|
||||
HTML (HyperText Markup Language) is the foundation of all webpages. Think of it as the framing of a house - it provides the basic structure that everything else builds upon.
|
||||
|
||||
### Key Concepts
|
||||
- HTML is a markup language, not a programming language
|
||||
- It tells browsers how to structure web content
|
||||
- Every HTML element is like a building block
|
||||
- Browsers interpret HTML to display content
|
||||
|
||||
## The Building Analogy
|
||||
When building a webpage, think of it like constructing a house:
|
||||
- **HTML**: The framing and structure (walls, rooms, layout)
|
||||
- **CSS**: The design elements (paint, decorations, styling)
|
||||
- **JavaScript**: The functionality (plumbing, electrical, moving parts)
|
||||
|
||||
## Basic HTML Structure
|
||||
|
||||
### 1. HTML Boilerplate
|
||||
Every webpage starts with a basic template:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Your Page Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Your content goes here -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 2. Understanding the Parts
|
||||
- `<!DOCTYPE html>`: Tells browsers this is an HTML5 document
|
||||
- `<html>`: The root element of the page
|
||||
- `<head>`: Contains metadata about the document
|
||||
- `<body>`: Contains the visible content
|
||||
|
||||
## Essential HTML Elements
|
||||
|
||||
### 1. Headings
|
||||
HTML has six levels of headings:
|
||||
```html
|
||||
<h1>Main Title</h1>
|
||||
<h2>Subtitle</h2>
|
||||
<h3>Section Header</h3>
|
||||
<!-- ... -->
|
||||
<h6>Smallest Heading</h6>
|
||||
```
|
||||
|
||||
### 2. Paragraphs
|
||||
```html
|
||||
<p>This is a paragraph of text. It can contain as much text as you need.</p>
|
||||
```
|
||||
|
||||
### 3. Images
|
||||
```html
|
||||
<img src="path-to-image.jpg" alt="Description of image" width="300">
|
||||
```
|
||||
|
||||
### 4. Links
|
||||
```html
|
||||
<a href="https://example.com">Click here</a>
|
||||
```
|
||||
|
||||
## HTML Attributes
|
||||
Attributes provide additional information or modify HTML elements:
|
||||
|
||||
```html
|
||||
<tag attribute="value">Content</tag>
|
||||
```
|
||||
|
||||
Common attributes:
|
||||
- `src`: Source path for images
|
||||
- `href`: Destination for links
|
||||
- `alt`: Alternative text for images
|
||||
- `class`: CSS class names
|
||||
- `id`: Unique identifier
|
||||
- `style`: Inline CSS styles
|
||||
|
||||
## Semantic HTML
|
||||
|
||||
### What is Semantic HTML?
|
||||
Semantic HTML uses meaningful tags that describe their content's purpose. This improves:
|
||||
- Accessibility
|
||||
- SEO (Search Engine Optimization)
|
||||
- Code readability
|
||||
- Maintainability
|
||||
|
||||
### Common Semantic Elements
|
||||
```html
|
||||
<header>
|
||||
<!-- Site header content -->
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<!-- Navigation menu -->
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<!-- Main content -->
|
||||
<article>
|
||||
<!-- Self-contained content -->
|
||||
</article>
|
||||
|
||||
<section>
|
||||
<!-- Grouped content -->
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<!-- Site footer content -->
|
||||
</footer>
|
||||
```
|
||||
|
||||
### Non-Semantic vs Semantic Example
|
||||
Instead of:
|
||||
```html
|
||||
<div class="header">
|
||||
<div class="navigation">
|
||||
<div class="nav-item">Home</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Use:
|
||||
```html
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
</nav>
|
||||
</header>
|
||||
```
|
||||
|
||||
## Building Your First Webpage
|
||||
|
||||
### 1. Basic Structure
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>My First Webpage</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Welcome to My First Webpage!</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>About Me</h2>
|
||||
<p>Hi, I'm learning web development with PlebDevs!</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>My Interests</h2>
|
||||
<p>I'm interested in Bitcoin, programming, and building cool stuff!</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Created by [Your Name] - 2024</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Structure
|
||||
- Use proper indentation
|
||||
- Keep code organized and readable
|
||||
- Use semantic elements when possible
|
||||
- Include all required elements (`DOCTYPE`, `html`, `head`, `body`)
|
||||
|
||||
### 2. Content
|
||||
- Use appropriate heading levels (start with `h1`)
|
||||
- Write descriptive `alt` text for images
|
||||
- Keep content meaningful and organized
|
||||
- Use comments to explain complex sections
|
||||
|
||||
### 3. Accessibility
|
||||
- Use semantic HTML elements
|
||||
- Provide alternative text for images
|
||||
- Maintain a logical heading structure
|
||||
- Ensure content makes sense when read linearly
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Problem: Images Not Loading
|
||||
```html
|
||||
<!-- Wrong -->
|
||||
<img src="image.jpg">
|
||||
|
||||
<!-- Right -->
|
||||
<img src="./images/image.jpg" alt="Description">
|
||||
```
|
||||
|
||||
### Problem: Links Not Working
|
||||
```html
|
||||
<!-- Wrong -->
|
||||
<a>Click here</a>
|
||||
|
||||
<!-- Right -->
|
||||
<a href="https://example.com">Click here</a>
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Practice Building**
|
||||
- Create a personal webpage about yourself
|
||||
- Include different types of content (text, images, links)
|
||||
- Use semantic HTML elements
|
||||
|
||||
2. **Experiment with Structure**
|
||||
- Try different layouts
|
||||
- Use various HTML elements
|
||||
- Pay attention to semantic meaning
|
||||
|
||||
3. **Prepare for CSS**
|
||||
- Think about how you want your page to look
|
||||
- Consider what styles you'll want to add
|
||||
- Plan your layout structure
|
||||
|
||||
## Exercise: Create Your Profile Page
|
||||
|
||||
Try creating a simple profile page using what you've learned:
|
||||
|
||||
1. Use the HTML boilerplate
|
||||
2. Add a header with your name
|
||||
3. Include an "About Me" section
|
||||
4. Add a photo (if you want)
|
||||
5. List your interests or goals
|
||||
6. Add a footer with contact information
|
||||
|
||||
Remember to:
|
||||
- Use semantic HTML
|
||||
- Include appropriate headings
|
||||
- Add descriptive alt text for images
|
||||
- Keep your code clean and well-organized
|
||||
|
||||
## Additional Resources
|
||||
- [MDN HTML Guide](https://developer.mozilla.org/en-US/docs/Web/HTML)
|
||||
- [HTML5 Doctor (Semantic Elements)](http://html5doctor.com/)
|
||||
- [W3Schools HTML Tutorial](https://www.w3schools.com/html/)
|
||||
|
||||
Remember: HTML is the foundation of web development. Take time to understand these basics well, as they'll serve as the building blocks for everything else you'll learn. Happy coding! 🚀
|
265
lesson-5.md
Normal file
265
lesson-5.md
Normal file
@ -0,0 +1,265 @@
|
||||
# CSS Fundamentals: Styling Your First Webpage
|
||||
|
||||
<!-- <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs.com/api/get-video-url?videoKey=starter-lesson-5.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div> -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs-bucket.nyc3.cdn.digitaloceanspaces.com/starter-lesson-4.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div>
|
||||
|
||||
## Introduction
|
||||
In our previous lesson, we created the structure of our webpage with HTML. Now, we'll learn how to style it using CSS (Cascading Style Sheets). While HTML provides the bones of our webpage, CSS adds the visual presentation - the colors, layouts, spacing, and overall aesthetics.
|
||||
|
||||
## What is CSS?
|
||||
|
||||
### Definition
|
||||
CSS (Cascading Style Sheets) is a stylesheet language that controls the visual presentation of HTML documents. Think of it like the paint, decorations, and interior design of a house - it determines how everything looks and is arranged.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
1. **Styling Capabilities**
|
||||
- Fonts and typography
|
||||
- Colors and backgrounds
|
||||
- Margins and padding
|
||||
- Element sizes
|
||||
- Visual effects
|
||||
- Layout and positioning
|
||||
|
||||
2. **Cascading Nature**
|
||||
- Styles can be inherited from parent elements
|
||||
- Multiple styles can apply to the same element
|
||||
- Specificity determines which styles take precedence
|
||||
- Styles "cascade" down through your document
|
||||
|
||||
## Basic CSS Syntax
|
||||
|
||||
```css
|
||||
selector {
|
||||
property: value;
|
||||
}
|
||||
```
|
||||
|
||||
### Example:
|
||||
```css
|
||||
h1 {
|
||||
color: blue;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
```
|
||||
|
||||
## Connecting CSS to HTML
|
||||
|
||||
### Method 1: External Stylesheet (Recommended)
|
||||
```html
|
||||
<link rel="stylesheet" href="style.css">
|
||||
```
|
||||
|
||||
### Method 2: Internal CSS
|
||||
```html
|
||||
<style>
|
||||
h1 {
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Method 3: Inline CSS (Use Sparingly)
|
||||
```html
|
||||
<h1 style="color: blue;">Title</h1>
|
||||
```
|
||||
|
||||
## The Box Model
|
||||
Every HTML element is treated as a box in CSS, with:
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Margin │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Border │ │
|
||||
│ │ ┌──────────┐ │ │
|
||||
│ │ │ Padding │ │ │
|
||||
│ │ │ ┌──────┐ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ │Content│ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ └──────┘ │ │ │
|
||||
│ │ └──────────┘ │ │
|
||||
│ └──────────────┘ │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
- **Content**: The actual content of the element
|
||||
- **Padding**: Space between content and border
|
||||
- **Border**: The border around the padding
|
||||
- **Margin**: Space outside the border
|
||||
|
||||
## CSS Units
|
||||
|
||||
### Absolute Units
|
||||
- `px` - pixels
|
||||
- `pt` - points
|
||||
- `cm` - centimeters
|
||||
- `mm` - millimeters
|
||||
- `in` - inches
|
||||
|
||||
### Relative Units
|
||||
- `%` - percentage relative to parent
|
||||
- `em` - relative to font-size
|
||||
- `rem` - relative to root font-size
|
||||
- `vh` - viewport height
|
||||
- `vw` - viewport width
|
||||
|
||||
## Practical Example: Styling Our Webpage
|
||||
|
||||
### 1. Basic Page Setup
|
||||
```css
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Header Styling
|
||||
```css
|
||||
header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Main Content Area
|
||||
```css
|
||||
main {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Footer Styling
|
||||
```css
|
||||
footer {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
## Layout with Flexbox
|
||||
|
||||
### Basic Concept
|
||||
Flexbox is a modern layout system that makes it easier to create flexible, responsive layouts.
|
||||
|
||||
### Key Properties
|
||||
```css
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row | column;
|
||||
justify-content: center | space-between | space-around;
|
||||
align-items: center | flex-start | flex-end;
|
||||
}
|
||||
```
|
||||
|
||||
### Common Use Cases
|
||||
1. Centering content
|
||||
2. Creating navigation bars
|
||||
3. Building responsive layouts
|
||||
4. Equal-height columns
|
||||
5. Dynamic spacing
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Organization
|
||||
- Use consistent naming conventions
|
||||
- Group related styles together
|
||||
- Comment your code for clarity
|
||||
- Keep selectors simple and specific
|
||||
|
||||
### 2. Performance
|
||||
- Avoid unnecessary specificity
|
||||
- Use shorthand properties when possible
|
||||
- Minimize redundant code
|
||||
- Consider load time impact
|
||||
|
||||
### 3. Maintainability
|
||||
- Use external stylesheets
|
||||
- Follow a consistent formatting style
|
||||
- Break large stylesheets into logical files
|
||||
- Document important design decisions
|
||||
|
||||
## Debugging CSS
|
||||
|
||||
### Common Tools
|
||||
1. Browser Developer Tools
|
||||
- Element inspector
|
||||
- Style inspector
|
||||
- Box model viewer
|
||||
|
||||
### Common Issues
|
||||
1. Specificity conflicts
|
||||
2. Inheritance problems
|
||||
3. Box model confusion
|
||||
4. Flexbox alignment issues
|
||||
|
||||
## Exercises
|
||||
|
||||
### 1. Style Modifications
|
||||
Try modifying these properties in your stylesheet:
|
||||
```css
|
||||
/* Change colors */
|
||||
header {
|
||||
background-color: #4a90e2;
|
||||
}
|
||||
|
||||
/* Adjust spacing */
|
||||
main {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* Modify typography */
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Layout Challenge
|
||||
Create a card layout using Flexbox:
|
||||
```css
|
||||
.card-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Learning Tools
|
||||
1. [Flexbox Froggy](https://flexboxfroggy.com/) - Interactive Flexbox learning game
|
||||
2. [CSS-Tricks](https://css-tricks.com) - Excellent CSS reference and tutorials
|
||||
3. [MDN CSS Documentation](https://developer.mozilla.org/en-US/docs/Web/CSS)
|
||||
|
||||
### Practice Projects
|
||||
1. Style your personal webpage
|
||||
2. Create a responsive navigation menu
|
||||
3. Build a flexible card layout
|
||||
4. Design a custom button style
|
||||
|
||||
Remember: CSS is both an art and a science. Don't be afraid to experiment and break things - that's how you'll learn the most. The key is to start simple and gradually add complexity as you become more comfortable with the basics.
|
||||
|
||||
Next up, we'll dive into JavaScript to add interactivity to our webpage! 🚀
|
202
lesson-6.md
Normal file
202
lesson-6.md
Normal file
@ -0,0 +1,202 @@
|
||||
# JavaScript: Building Your First Interactive Web App
|
||||
|
||||
<!-- <div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs.com/api/get-video-url?videoKey=starter-lesson-5.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div> -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="https://plebdevs-bucket.nyc3.cdn.digitaloceanspaces.com/starter-lesson-5.mp4" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: 0px;" controls=""></video></div>
|
||||
|
||||
## Introduction
|
||||
In this lesson, we'll bring our web pages to life by adding dynamic functionality with JavaScript. We'll build a real-world application that displays and updates Bitcoin prices in real-time, teaching core JavaScript concepts along the way.
|
||||
|
||||
## Project Overview: Bitcoin Price Tracker
|
||||
We'll build a web application that:
|
||||
- Displays current Bitcoin price
|
||||
- Updates automatically every 3 seconds
|
||||
- Allows currency switching
|
||||
- Includes interactive controls
|
||||
- Shows current date/time
|
||||
|
||||
## Core JavaScript Concepts
|
||||
|
||||
### 1. Variables and Data Types
|
||||
```javascript
|
||||
// Variables can be declared with let or const
|
||||
let currentCurrency = "USD"; // Can be changed
|
||||
const interval = 3000; // Cannot be changed
|
||||
|
||||
// Basic data types
|
||||
const price = 45000; // Number
|
||||
const isVisible = true; // Boolean
|
||||
const currency = "USD"; // String
|
||||
```
|
||||
|
||||
### 2. DOM Manipulation
|
||||
```javascript
|
||||
// Getting elements
|
||||
const priceElement = document.getElementById('price');
|
||||
const button = document.getElementById('refresh-button');
|
||||
|
||||
// Modifying content
|
||||
priceElement.textContent = `${price} ${currency}`;
|
||||
|
||||
// Changing styles
|
||||
priceElement.style.display = 'none';
|
||||
```
|
||||
|
||||
### 3. Event Listeners
|
||||
```javascript
|
||||
// Basic click handler
|
||||
button.addEventListener('click', () => {
|
||||
fetchBitcoinPrice();
|
||||
});
|
||||
|
||||
// Change event for select elements
|
||||
selector.addEventListener('change', (event) => {
|
||||
handleCurrencyChange(event.value);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Async Operations & Fetch API
|
||||
```javascript
|
||||
async function fetchBitcoinPrice() {
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
updatePrice(data.price);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
### HTML Setup
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Bitcoin Price Tracker</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="index.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Current Bitcoin Price</h1>
|
||||
<p>The price is: <span id="price"></span></p>
|
||||
<!-- Additional elements -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Core Functionality Implementation
|
||||
|
||||
1. **Setting Up the Timer**
|
||||
```javascript
|
||||
// Update price every 3 seconds
|
||||
setInterval(fetchBitcoinPrice, 3000);
|
||||
|
||||
// Update date/time every second
|
||||
setInterval(updateDateTime, 1000);
|
||||
```
|
||||
|
||||
2. **Currency Selection**
|
||||
```javascript
|
||||
function handleCurrencyChange(newCurrency) {
|
||||
currentCurrency = newCurrency;
|
||||
fetchBitcoinPrice();
|
||||
}
|
||||
```
|
||||
|
||||
3. **Toggle Visibility**
|
||||
```javascript
|
||||
function togglePriceVisibility() {
|
||||
const price = document.getElementById('price');
|
||||
price.style.display = price.style.display === 'none'
|
||||
? 'inline'
|
||||
: 'none';
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Error Handling
|
||||
- Always use try/catch with async operations
|
||||
- Provide meaningful error messages
|
||||
- Handle edge cases gracefully
|
||||
|
||||
### 2. Code Organization
|
||||
- Keep functions focused and small
|
||||
- Use meaningful variable names
|
||||
- Group related functionality
|
||||
- Add comments for clarity
|
||||
|
||||
### 3. Performance
|
||||
- Avoid unnecessary DOM updates
|
||||
- Use appropriate update intervals
|
||||
- Clean up intervals when not needed
|
||||
|
||||
## Common Challenges & Solutions
|
||||
|
||||
### 1. API Issues
|
||||
```javascript
|
||||
// Handle API failures gracefully
|
||||
catch (error) {
|
||||
priceElement.textContent = 'Price unavailable';
|
||||
console.error('API Error:', error);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Currency Formatting
|
||||
```javascript
|
||||
function formatPrice(price, currency) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(price);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Time Zones
|
||||
```javascript
|
||||
function getLocalTime() {
|
||||
return new Date().toLocaleString();
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the Project
|
||||
Consider adding these features for practice:
|
||||
1. Price change indicators (up/down arrows)
|
||||
2. Historical price chart
|
||||
3. Multiple cryptocurrency support
|
||||
4. Price alerts
|
||||
5. Local storage for settings
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Using Console
|
||||
```javascript
|
||||
console.log('Price fetched:', price);
|
||||
console.error('Error occurred:', error);
|
||||
console.table(priceHistory);
|
||||
```
|
||||
|
||||
### Chrome DevTools
|
||||
1. Network tab for API calls
|
||||
2. Console for errors
|
||||
3. Elements for DOM inspection
|
||||
4. Sources for debugging
|
||||
|
||||
## Additional Resources
|
||||
- MDN JavaScript Guide
|
||||
- JavaScript.info
|
||||
- CoinGecko API Documentation
|
||||
- Chrome DevTools Documentation
|
||||
|
||||
## Next Steps
|
||||
1. Add styling with CSS
|
||||
2. Implement additional features
|
||||
3. Learn about React for more complex applications
|
||||
4. Explore other APIs and cryptocurrencies
|
||||
|
||||
Remember: The best way to learn is by doing. Don't be afraid to break things and experiment with the code. The developer console is your friend for debugging and understanding what's happening in your application.
|
||||
|
||||
Happy coding! 🚀
|
@ -59,14 +59,13 @@ export default function DocumentsCarousel() {
|
||||
// Sort documents by created_at in descending order (most recent first)
|
||||
const sortedDocuments = processedDocuments.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
if (paidLessons && paidLessons.length > 0) {
|
||||
// filter out documents that are in the paid lessons array
|
||||
const filteredDocuments = sortedDocuments.filter(document => !paidLessons.includes(document?.d));
|
||||
// Filter out documents that are in paid lessons and combined resources
|
||||
const filteredDocuments = sortedDocuments.filter(document =>
|
||||
!paidLessons.includes(document?.d) &&
|
||||
!(document.topics?.includes('video') && document.topics?.includes('document'))
|
||||
);
|
||||
|
||||
setProcessedDocuments(filteredDocuments);
|
||||
} else {
|
||||
setProcessedDocuments(sortedDocuments);
|
||||
}
|
||||
setProcessedDocuments(filteredDocuments);
|
||||
} else {
|
||||
console.log('No documents fetched or empty array returned');
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateS
|
||||
import { VideoTemplate } from '@/components/content/carousels/templates/VideoTemplate';
|
||||
import { DocumentTemplate } from '@/components/content/carousels/templates/DocumentTemplate';
|
||||
import { CourseTemplate } from '@/components/content/carousels/templates/CourseTemplate';
|
||||
import { CombinedTemplate } from '@/components/content/carousels/templates/CombinedTemplate';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const responsiveOptions = [
|
||||
@ -74,7 +75,9 @@ export default function GenericCarousel({items, selectedTopic, title}) {
|
||||
value={carouselItems}
|
||||
itemTemplate={(item) => {
|
||||
if (carouselItems.length > 0) {
|
||||
if (item.type === 'document') {
|
||||
if (item.topics?.includes('video') && item.topics?.includes('document')) {
|
||||
return <CombinedTemplate key={item.id} resource={item} isLesson={lessons.includes(item?.d)} />;
|
||||
} else if (item.type === 'document') {
|
||||
return <DocumentTemplate key={item.id} document={item} isLesson={lessons.includes(item?.d)} />;
|
||||
} else if (item.type === 'video') {
|
||||
return <VideoTemplate key={item.id} video={item} isLesson={lessons.includes(item?.d)} />;
|
||||
|
@ -62,14 +62,13 @@ export default function VideosCarousel() {
|
||||
|
||||
const sortedVideos = processedVideos.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
if (paidLessons && paidLessons.length > 0) {
|
||||
// filter out videos that are in the paid lessons array
|
||||
const filteredVideos = sortedVideos.filter(video => !paidLessons.includes(video?.d));
|
||||
// Filter out videos that are in paid lessons and combined resources
|
||||
const filteredVideos = sortedVideos.filter(video =>
|
||||
!paidLessons.includes(video?.d) &&
|
||||
!(video.topics?.includes('video') && video.topics?.includes('document'))
|
||||
);
|
||||
|
||||
setProcessedVideos(filteredVideos);
|
||||
} else {
|
||||
setProcessedVideos(sortedVideos);
|
||||
}
|
||||
setProcessedVideos(filteredVideos);
|
||||
} else {
|
||||
console.log('No videos fetched or empty array returned');
|
||||
}
|
||||
|
127
src/components/content/carousels/templates/CombinedTemplate.js
Normal file
127
src/components/content/carousels/templates/CombinedTemplate.js
Normal file
@ -0,0 +1,127 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import Image from "next/image"
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useRouter } from "next/router";
|
||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Tag } from "primereact/tag";
|
||||
import { Message } from "primereact/message";
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { PlayCircle, FileText } from "lucide-react";
|
||||
|
||||
export function CombinedTemplate({ resource, isLesson, showMetaTags }) {
|
||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: resource });
|
||||
const [nAddress, setNAddress] = useState(null);
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const windowWidth = useWindowWidth();
|
||||
const isMobile = windowWidth < 768;
|
||||
|
||||
useEffect(() => {
|
||||
if (resource && resource?.d) {
|
||||
const nAddress = nip19.naddrEncode({
|
||||
pubkey: resource.pubkey,
|
||||
kind: resource.kind,
|
||||
identifier: resource.d
|
||||
});
|
||||
setNAddress(nAddress);
|
||||
}
|
||||
}, [resource]);
|
||||
|
||||
useEffect(() => {
|
||||
if (zaps.length > 0) {
|
||||
const total = getTotalFromZaps(zaps, resource);
|
||||
setZapAmount(total);
|
||||
}
|
||||
}, [zaps, resource]);
|
||||
|
||||
const shouldShowMetaTags = (topic) => {
|
||||
if (!showMetaTags) {
|
||||
return !["lesson", "document", "video", "course"].includes(topic);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (zapsError) return <div>Error: {zapsError}</div>;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden group hover:shadow-xl transition-all duration-300 bg-gray-800 m-2 border-none">
|
||||
<div className="relative w-full h-0" style={{ paddingBottom: "56.25%" }}>
|
||||
<Image
|
||||
alt="resource thumbnail"
|
||||
src={returnImageProxy(resource.image)}
|
||||
quality={100}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-md"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
||||
<div className="absolute top-4 right-4 flex items-center gap-1 bg-black/50 text-white px-3 py-1 rounded-full">
|
||||
<ZapDisplay zapAmount={zapAmount} event={resource} zapsLoading={zapsLoading && zapAmount === 0} />
|
||||
</div>
|
||||
<div className="absolute bottom-4 left-4 flex gap-2">
|
||||
<PlayCircle className="w-6 h-6 text-white" />
|
||||
<FileText className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<CardHeader className="flex flex-row justify-between items-center p-4 border-b border-gray-700">
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="text-xl sm:text-2xl text-[#f8f8ff]">{resource.title}</CardTitle>
|
||||
</div>
|
||||
<div>
|
||||
{resource?.price && resource?.price > 0 ? (
|
||||
<Message className={`${isMobile ? "py-1 text-xs" : "py-2"} whitespace-nowrap`} icon="pi pi-lock" severity="info" text={`${resource.price} sats`} />
|
||||
) : (
|
||||
<Message className={`${isMobile ? "py-1 text-xs" : "py-2"} whitespace-nowrap`} icon="pi pi-lock-open" severity="success" text="Free" />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className={`${isMobile ? "px-3" : ""} pt-6 pb-2 w-full flex flex-row justify-between items-start`}>
|
||||
<div className="flex flex-wrap gap-2 max-w-[65%]">
|
||||
{resource?.topics?.map((topic, index) => (
|
||||
shouldShowMetaTags(topic) && (
|
||||
<Tag size="small" key={index} className="px-2 py-1 text-sm text-[#f8f8ff]">
|
||||
{topic}
|
||||
</Tag>
|
||||
)
|
||||
))}
|
||||
{isLesson && showMetaTags && <Tag size="small" className="px-2 py-1 text-sm text-[#f8f8ff]" value="lesson" />}
|
||||
</div>
|
||||
<p className="font-bold text-gray-300">Video / Document</p>
|
||||
</CardContent>
|
||||
<CardDescription className={`${isMobile ? "w-full p-3" : "p-6"} py-2 pt-0 text-base text-neutral-50/90 dark:text-neutral-900/90 overflow-hidden min-h-[4em] flex items-center max-w-[100%]`}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: "2"
|
||||
}}>
|
||||
<p className="line-clamp-2 text-wrap break-words">{(resource.summary || resource.description)?.split('\n').map((line, index) => (
|
||||
<span className="text-wrap break-words" key={index}>{line}</span>
|
||||
))}</p>
|
||||
</CardDescription>
|
||||
<CardFooter className={`flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-t border-gray-700 pt-4 ${isMobile ? "px-3" : ""}`}>
|
||||
<p className="text-sm text-gray-300">{resource?.published_at && resource.published_at !== "" ? (
|
||||
formatTimestampToHowLongAgo(resource.published_at)
|
||||
) : (
|
||||
formatTimestampToHowLongAgo(resource.created_at)
|
||||
)}</p>
|
||||
<GenericButton
|
||||
onClick={() => router.push(`/details/${nAddress}`)}
|
||||
size="small"
|
||||
label="View"
|
||||
icon="pi pi-chevron-right"
|
||||
iconPos="right"
|
||||
outlined
|
||||
className="items-center py-2"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
250
src/components/content/combined/CombinedDetails.js
Normal file
250
src/components/content/combined/CombinedDetails.js
Normal file
@ -0,0 +1,250 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { Tag } from "primereact/tag";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import ResourcePaymentButton from "@/components/bitcoinConnect/ResourcePaymentButton";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const CombinedDetails = ({ processedEvent, topics, title, summary, image, price, author, paidResource, decryptedContent, nAddress, handlePaymentSuccess, handlePaymentError, authorView, isLesson }) => {
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const [course, setCourse] = useState(null);
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const { zaps, zapsLoading } = useZapsSubscription({ event: processedEvent });
|
||||
const { data: session } = useSession();
|
||||
const { showToast } = useToast();
|
||||
const windowWidth = useWindowWidth();
|
||||
const isMobileView = windowWidth <= 768;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLesson) {
|
||||
axios.get(`/api/resources/${processedEvent.d}`).then(res => {
|
||||
if (res.data && res.data.lessons[0]?.courseId) {
|
||||
setCourse(res.data.lessons[0]?.courseId);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('err', err);
|
||||
});
|
||||
}
|
||||
}, [processedEvent.d, isLesson]);
|
||||
|
||||
useEffect(() => {
|
||||
if (zaps.length > 0) {
|
||||
const total = getTotalFromZaps(zaps, processedEvent);
|
||||
setZapAmount(total);
|
||||
}
|
||||
}, [zaps, processedEvent]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
const response = await axios.delete(`/api/resources/${processedEvent.d}`);
|
||||
if (response.status === 204) {
|
||||
showToast('success', 'Success', 'Resource deleted successfully.');
|
||||
router.push('/');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response?.data?.error?.includes("Invalid `prisma.resource.delete()`")) {
|
||||
showToast('error', 'Error', 'Resource cannot be deleted because it is part of a course, delete the course first.');
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to delete resource. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderPaymentMessage = () => {
|
||||
if (session?.user?.role?.subscribed && decryptedContent) {
|
||||
return <GenericButton tooltipOptions={{ position: 'top' }} tooltip="You are subscribed so you can access all paid content" icon="pi pi-check" label="Subscribed" severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
if (isLesson && course && session?.user?.purchased?.some(purchase => purchase.courseId === course)) {
|
||||
const coursePurchase = session?.user?.purchased?.find(purchase => purchase.courseId === course);
|
||||
return <GenericButton tooltipOptions={{ position: 'top' }} tooltip={`You have this lesson through purchasing the course it belongs to. You paid ${coursePurchase?.course?.price} sats for the course.`} icon="pi pi-check" label={`Paid ${coursePurchase?.course?.price} sats`} severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
if (paidResource && decryptedContent && author && processedEvent?.pubkey !== session?.user?.pubkey && !session?.user?.role?.subscribed) {
|
||||
return <GenericButton icon="pi pi-check" label={`Paid ${processedEvent.price} sats`} severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
if (paidResource && author && processedEvent?.pubkey === session?.user?.pubkey) {
|
||||
return <GenericButton tooltipOptions={{ position: 'top' }} tooltip={`You created this paid content, users must pay ${processedEvent.price} sats to access it`} icon="pi pi-check" label={`Price ${processedEvent.price} sats`} severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (decryptedContent) {
|
||||
return (
|
||||
<MDDisplay className='p-2 rounded-lg w-full' source={decryptedContent} />
|
||||
);
|
||||
}
|
||||
|
||||
if (paidResource && !decryptedContent) {
|
||||
return (
|
||||
<div className="w-full px-4">
|
||||
<div className="w-full p-8 rounded-lg flex flex-col items-center justify-center bg-gray-800">
|
||||
<div className="mx-auto py-auto">
|
||||
<i className="pi pi-lock text-[60px] text-red-500"></i>
|
||||
</div>
|
||||
<p className="text-center text-xl text-red-500 mt-4">
|
||||
This content is paid and needs to be purchased before viewing.
|
||||
</p>
|
||||
<div className="flex flex-row items-center justify-center w-full mt-4">
|
||||
<ResourcePaymentButton
|
||||
lnAddress={author?.lud16}
|
||||
amount={price}
|
||||
onSuccess={handlePaymentSuccess}
|
||||
onError={handlePaymentError}
|
||||
resourceId={processedEvent.d}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderAdditionalLinks = () => {
|
||||
if (processedEvent?.additionalLinks?.length > 0) {
|
||||
return (
|
||||
<div className="my-4">
|
||||
<p>Additional Links:</p>
|
||||
{processedEvent.additionalLinks.map((link, index) => (
|
||||
<div key={index} className="mb-2">
|
||||
<a
|
||||
className="text-blue-500 hover:underline hover:text-blue-600 break-words"
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
display: 'inline-block',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="relative w-full h-[400px] mb-8">
|
||||
<Image
|
||||
alt="background image"
|
||||
src={returnImageProxy(image)}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-20"></div>
|
||||
</div>
|
||||
<div className="w-full mx-auto px-4 py-8 -mt-32 relative z-10 max-mob:px-0 max-tab:px-0">
|
||||
<div className="mb-8 bg-gray-800/70 rounded-lg p-4 max-mob:rounded-t-none max-tab:rounded-t-none">
|
||||
<div className="flex flex-row items-center justify-between w-full">
|
||||
<h1 className='text-4xl font-bold text-white'>{title}</h1>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{topics?.map((topic, index) => (
|
||||
<Tag className='text-[#f8f8ff]' key={index} value={topic} />
|
||||
))}
|
||||
{isLesson && <Tag size="small" className="text-[#f8f8ff]" value="lesson" />}
|
||||
</div>
|
||||
</div>
|
||||
{summary?.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
{renderAdditionalLinks()}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Image
|
||||
alt="avatar image"
|
||||
src={returnImageProxy(author?.avatar, author?.username)}
|
||||
width={50}
|
||||
height={50}
|
||||
className="rounded-full mr-4"
|
||||
/>
|
||||
<p className='text-lg text-white'>
|
||||
By{' '}
|
||||
<a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'>
|
||||
{author?.username}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={processedEvent}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-full mt-8 flex flex-wrap justify-between items-center'>
|
||||
{authorView ? (
|
||||
<div className='flex space-x-2 mt-4 sm:mt-0'>
|
||||
{renderPaymentMessage()}
|
||||
<div className="flex flex-row gap-2">
|
||||
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
|
||||
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
|
||||
<GenericButton
|
||||
tooltip={isMobileView ? null : "View Nostr Note"}
|
||||
tooltipOptions={{ position: 'left' }}
|
||||
icon="pi pi-external-link"
|
||||
outlined
|
||||
onClick={() => window.open(`https://habla.news/a/${nAddress}`, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex flex-row justify-between gap-2">
|
||||
{renderPaymentMessage()}
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{course && (
|
||||
<GenericButton
|
||||
size={isMobileView ? 'small' : null}
|
||||
outlined
|
||||
icon="pi pi-external-link"
|
||||
onClick={() => window.open(`/course/${course}`, '_blank')}
|
||||
label={isMobileView ? "Course" : "Open Course"}
|
||||
tooltip="This is a lesson in a course"
|
||||
tooltipOptions={{ position: 'top' }}
|
||||
/>
|
||||
)}
|
||||
<GenericButton
|
||||
size={isMobileView ? 'small' : null}
|
||||
tooltip={isMobileView ? null : "View Nostr Note"}
|
||||
tooltipOptions={{ position: 'left' }}
|
||||
icon="pi pi-external-link"
|
||||
outlined
|
||||
onClick={() => window.open(`https://habla.news/a/${nAddress}`, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CombinedDetails;
|
294
src/components/forms/CombinedResourceForm.js
Normal file
294
src/components/forms/CombinedResourceForm.js
Normal file
@ -0,0 +1,294 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'next/router';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { InputSwitch } from 'primereact/inputswitch';
|
||||
import GenericButton from '@/components/buttons/GenericButton';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import { useEncryptContent } from '@/hooks/encryption/useEncryptContent';
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import 'primeicons/primeicons.css';
|
||||
import 'primereact/resources/primereact.min.css';
|
||||
|
||||
const MDEditor = dynamic(
|
||||
() => import("@uiw/react-md-editor"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const CombinedResourceForm = ({ draft = null, isPublished = false }) => {
|
||||
const [title, setTitle] = useState(draft?.title || '');
|
||||
const [summary, setSummary] = useState(draft?.summary || '');
|
||||
const [price, setPrice] = useState(draft?.price || 0);
|
||||
const [isPaidResource, setIsPaidResource] = useState(draft?.price ? true : false);
|
||||
const [videoUrl, setVideoUrl] = useState(draft?.videoUrl || '');
|
||||
const [content, setContent] = useState(draft?.content || '');
|
||||
const [coverImage, setCoverImage] = useState(draft?.image || '');
|
||||
const [topics, setTopics] = useState(draft?.topics || ['']);
|
||||
const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']);
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const { showToast } = useToast();
|
||||
const { ndk, addSigner } = useNDKContext();
|
||||
const { encryptContent } = useEncryptContent();
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
setUser(session.user);
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const handleContentChange = useCallback((value) => {
|
||||
setContent(value || '');
|
||||
}, []);
|
||||
|
||||
const getVideoEmbed = (url) => {
|
||||
let embedCode = '';
|
||||
|
||||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||||
const videoId = url.split('v=')[1] || url.split('/').pop();
|
||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://www.youtube.com/embed/${videoId}?enablejsapi=1" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
||||
} else if (url.includes('vimeo.com')) {
|
||||
const videoId = url.split('/').pop();
|
||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://player.vimeo.com/video/${videoId}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
||||
} else if (url.includes('.mp4') || url.includes('.mov') || url.includes('.avi') || url.includes('.wmv') || url.includes('.flv') || url.includes('.webm')) {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
|
||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="${baseUrl}/api/get-video-url?videoKey=${encodeURIComponent(url)}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" controls></video></div>`;
|
||||
}
|
||||
|
||||
return embedCode;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const userResponse = await axios.get(`/api/users/${user.pubkey}`);
|
||||
if (!userResponse.data) {
|
||||
showToast('error', 'Error', 'User not found', 'Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoEmbed = videoUrl ? getVideoEmbed(videoUrl) : '';
|
||||
const combinedContent = `${videoEmbed}\n\n${content}`;
|
||||
|
||||
const payload = {
|
||||
title,
|
||||
summary,
|
||||
type: 'combined',
|
||||
price: isPaidResource ? price : null,
|
||||
content: combinedContent,
|
||||
image: coverImage,
|
||||
user: userResponse.data.id,
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'video', 'document'])],
|
||||
additionalLinks: additionalLinks.filter(link => link.trim() !== ''),
|
||||
};
|
||||
|
||||
const url = draft ? `/api/drafts/${draft.id}` : '/api/drafts';
|
||||
const method = draft ? 'put' : 'post';
|
||||
|
||||
try {
|
||||
const response = await axios[method](url, payload);
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
showToast('success', 'Success', draft ? 'Content updated successfully.' : 'Content saved as draft.');
|
||||
if (response.data?.id) {
|
||||
router.push(`/draft/${response.data.id}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('error', 'Error', 'Failed to save content. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTopicChange = (index, value) => {
|
||||
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
|
||||
setTopics(updatedTopics);
|
||||
};
|
||||
|
||||
const addTopic = (e) => {
|
||||
e.preventDefault();
|
||||
setTopics([...topics, '']);
|
||||
};
|
||||
|
||||
const removeTopic = (e, index) => {
|
||||
e.preventDefault();
|
||||
const updatedTopics = topics.filter((_, i) => i !== index);
|
||||
setTopics(updatedTopics);
|
||||
};
|
||||
|
||||
const handleAdditionalLinkChange = (index, value) => {
|
||||
const updatedAdditionalLinks = additionalLinks.map((link, i) => i === index ? value : link);
|
||||
setAdditionalLinks(updatedAdditionalLinks);
|
||||
};
|
||||
|
||||
const addAdditionalLink = (e) => {
|
||||
e.preventDefault();
|
||||
setAdditionalLinks([...additionalLinks, '']);
|
||||
};
|
||||
|
||||
const removeAdditionalLink = (e, index) => {
|
||||
e.preventDefault();
|
||||
const updatedAdditionalLinks = additionalLinks.filter((_, i) => i !== index);
|
||||
setAdditionalLinks(updatedAdditionalLinks);
|
||||
};
|
||||
|
||||
const buildEvent = async (draft) => {
|
||||
const dTag = draft.d;
|
||||
const event = new NDKEvent(ndk);
|
||||
let encryptedContent;
|
||||
|
||||
const videoEmbed = videoUrl ? getVideoEmbed(videoUrl) : '';
|
||||
const combinedContent = `${videoEmbed}\n\n${content}`;
|
||||
|
||||
if (draft?.price) {
|
||||
encryptedContent = await encryptContent(combinedContent);
|
||||
}
|
||||
|
||||
event.kind = draft?.price ? 30402 : 30023;
|
||||
event.content = draft?.price ? encryptedContent : combinedContent;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = user.pubkey;
|
||||
event.tags = [
|
||||
['d', dTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||
];
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
const handlePublishedResource = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const updatedDraft = {
|
||||
title,
|
||||
summary,
|
||||
price,
|
||||
content,
|
||||
videoUrl,
|
||||
d: draft.d,
|
||||
image: coverImage,
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'video', 'document'])],
|
||||
additionalLinks: additionalLinks.filter(link => link.trim() !== '')
|
||||
};
|
||||
|
||||
const event = await buildEvent(updatedDraft);
|
||||
|
||||
try {
|
||||
if (!ndk.signer) {
|
||||
await addSigner();
|
||||
}
|
||||
|
||||
await ndk.connect();
|
||||
|
||||
const published = await ndk.publish(event);
|
||||
|
||||
if (published) {
|
||||
const response = await axios.put(`/api/resources/${draft.d}`, { noteId: event.id });
|
||||
showToast('success', 'Success', 'Content published successfully.');
|
||||
router.push(`/details/${event.id}`);
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to publish content. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('error', 'Error', 'Failed to publish content. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="p-inputgroup flex-1">
|
||||
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputTextarea value={summary} onChange={(e) => setSummary(e.target.value)} placeholder="Summary" rows={5} cols={30} />
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4 flex-col">
|
||||
<p className="py-2">Paid Resource</p>
|
||||
<InputSwitch checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
|
||||
{isPaidResource && (
|
||||
<div className="p-inputgroup flex-1 py-4">
|
||||
<i className="pi pi-bolt p-inputgroup-addon text-2xl text-yellow-500"></i>
|
||||
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputText value={videoUrl} onChange={(e) => setVideoUrl(e.target.value)} placeholder="Video URL (YouTube, Vimeo, or direct video link)" />
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" />
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 flex-col mt-4">
|
||||
<span>Content</span>
|
||||
<div data-color-mode="dark">
|
||||
<MDEditor
|
||||
value={content}
|
||||
onChange={handleContentChange}
|
||||
height={350}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex-col w-full">
|
||||
<span className="pl-1 flex items-center">
|
||||
External Links
|
||||
<i className="pi pi-info-circle ml-2 cursor-pointer"
|
||||
data-pr-tooltip="Add any relevant external links that pair with this content (these links are currently not encrypted for 'paid' content)"
|
||||
data-pr-position="right"
|
||||
data-pr-at="right+5 top"
|
||||
data-pr-my="left center-2"
|
||||
style={{ fontSize: '1rem', color: 'var(--primary-color)' }}
|
||||
/>
|
||||
</span>
|
||||
{additionalLinks.map((link, index) => (
|
||||
<div className="p-inputgroup flex-1" key={index}>
|
||||
<InputText value={link} onChange={(e) => handleAdditionalLinkChange(index, e.target.value)} placeholder="https://plebdevs.com" className="w-full mt-2" />
|
||||
{index > 0 && (
|
||||
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeAdditionalLink(e, index)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="w-full flex flex-row items-end justify-end py-2">
|
||||
<GenericButton icon="pi pi-plus" onClick={addAdditionalLink} />
|
||||
</div>
|
||||
<Tooltip target=".pi-info-circle" />
|
||||
</div>
|
||||
<div className="mt-8 flex-col w-full">
|
||||
{topics.map((topic, index) => (
|
||||
<div className="p-inputgroup flex-1" key={index}>
|
||||
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" />
|
||||
{index > 0 && (
|
||||
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="w-full flex flex-row items-end justify-end py-2">
|
||||
<GenericButton icon="pi pi-plus" onClick={addTopic} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-8">
|
||||
<GenericButton type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CombinedResourceForm;
|
@ -11,7 +11,7 @@ const appConfig = {
|
||||
"wss://purplerelay.com/",
|
||||
"wss://relay.devs.tools/"
|
||||
],
|
||||
authorPubkeys: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741", "c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345"],
|
||||
authorPubkeys: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741", "c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345", "468f729dd409053dac5e7470622c3996aad88db6ed1de9165cb1921b5ab4fd5e"],
|
||||
customLightningAddresses: [
|
||||
{
|
||||
// todo remove need for lowercase
|
||||
|
@ -3,6 +3,7 @@ import MenuTab from "@/components/menutab/MenuTab";
|
||||
import DocumentForm from "@/components/forms/DocumentForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import CombinedResourceForm from "@/components/forms/CombinedResourceForm";
|
||||
import { useIsAdmin } from "@/hooks/useIsAdmin";
|
||||
import { useRouter } from "next/router";
|
||||
import { ProgressSpinner } from "primereact/progressspinner";
|
||||
@ -14,6 +15,7 @@ const Create = () => {
|
||||
const homeItems = [
|
||||
{ label: 'Document', icon: 'pi pi-file' },
|
||||
{ label: 'Video', icon: 'pi pi-video' },
|
||||
{ label: 'Combined', icon: 'pi pi-clone' },
|
||||
{ label: 'Course', icon: 'pi pi-desktop' }
|
||||
];
|
||||
|
||||
@ -34,8 +36,10 @@ const Create = () => {
|
||||
return <VideoForm />;
|
||||
case 'Document':
|
||||
return <DocumentForm />;
|
||||
case 'Combined':
|
||||
return <CombinedResourceForm />;
|
||||
default:
|
||||
return null; // or a default component
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { parseEvent } from "@/utils/nostr";
|
||||
import DocumentForm from "@/components/forms/DocumentForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import CombinedResourceForm from "@/components/forms/CombinedResourceForm";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
|
||||
@ -39,8 +40,9 @@ export default function Edit() {
|
||||
<div className="w-[80vw] max-w-[80vw] mx-auto my-8 flex flex-col justify-center">
|
||||
<h2 className="text-center mb-8">Edit Published Event</h2>
|
||||
{event?.topics.includes('course') && <CourseForm draft={event} isPublished />}
|
||||
{!event?.topics.includes('video') && <VideoForm draft={event} isPublished />}
|
||||
{event?.topics.includes('document') && <DocumentForm draft={event} isPublished />}
|
||||
{event?.topics.includes('video') && !event?.topics.includes('document') && <VideoForm draft={event} isPublished />}
|
||||
{event?.topics.includes('document') && !event?.topics.includes('video') && <DocumentForm draft={event} isPublished />}
|
||||
{event?.topics.includes('video') && event?.topics.includes('document') && <CombinedResourceForm draft={event} isPublished />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { useRouter } from "next/router";
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import axios from 'axios';
|
||||
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
||||
import CombinedDetails from "@/components/content/combined/CombinedDetails";
|
||||
|
||||
// todo: /decrypt is still being called way too much on this page, need to clean up state management
|
||||
|
||||
@ -158,7 +159,14 @@ const Details = () => {
|
||||
|
||||
if (!author || !event) return null;
|
||||
|
||||
const DetailComponent = event.type === "document" ? DocumentDetails : VideoDetails;
|
||||
const getDetailComponent = () => {
|
||||
if (event.topics.includes('video') && event.topics.includes('document')) {
|
||||
return CombinedDetails;
|
||||
}
|
||||
return event.type === "document" ? DocumentDetails : VideoDetails;
|
||||
};
|
||||
|
||||
const DetailComponent = getDetailComponent();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -4,6 +4,7 @@ import axios from "axios";
|
||||
import DocumentForm from "@/components/forms/DocumentForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import CombinedResourceForm from "@/components/forms/CombinedResourceForm";
|
||||
import { useIsAdmin } from "@/hooks/useIsAdmin";
|
||||
|
||||
const Edit = () => {
|
||||
@ -38,6 +39,7 @@ const Edit = () => {
|
||||
{draft?.type === 'course' && <CourseForm draft={draft} />}
|
||||
{draft?.type === 'video' && <VideoForm draft={draft} />}
|
||||
{draft?.type === 'document' && <DocumentForm draft={draft} />}
|
||||
{draft?.type === 'combined' && <CombinedResourceForm draft={draft} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -116,8 +116,8 @@ export default function Draft() {
|
||||
|
||||
const handlePostResource = async (resource, videoId) => {
|
||||
const dTag = resource.tags.find(tag => tag[0] === 'd')[1];
|
||||
let price
|
||||
|
||||
let price
|
||||
|
||||
try {
|
||||
price = resource.tags.find(tag => tag[0] === 'price')[1];
|
||||
} catch (err) {
|
||||
@ -241,6 +241,33 @@ export default function Draft() {
|
||||
|
||||
type = 'video';
|
||||
break;
|
||||
case 'combined':
|
||||
if (draft?.price) {
|
||||
encryptedContent = await encryptContent(draft.content);
|
||||
}
|
||||
|
||||
if (draft?.content.includes('?videoKey=')) {
|
||||
const extractedVideoId = draft.content.split('?videoKey=')[1].split('"')[0];
|
||||
videoId = extractedVideoId;
|
||||
}
|
||||
|
||||
event.kind = draft?.price ? 30402 : 30023;
|
||||
event.content = draft?.price ? encryptedContent : draft.content;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = user.pubkey;
|
||||
event.tags = [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||
...(draft?.additionalLinks ? draft.additionalLinks.filter(link => link !== 'https://plebdevs.com').map(link => ['r', link]) : []),
|
||||
];
|
||||
|
||||
type = 'combined';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -264,13 +291,13 @@ export default function Draft() {
|
||||
</div>
|
||||
<h1 className='text-4xl mt-4'>{draft?.title}</h1>
|
||||
<p className='text-xl mt-4'>{draft?.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{draft.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<div className="text-xl mt-4">
|
||||
{draft.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
{draft?.price && (
|
||||
<p className='text-lg mt-4'>Price: {draft.price}</p>
|
||||
)}
|
||||
@ -305,7 +332,7 @@ export default function Draft() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="pt-8 text-sm text-gray-400">{draft?.createdAt && formatDateTime(draft?.createdAt)}</p>
|
||||
<p className="pt-8 text-sm text-gray-400">{draft?.createdAt && formatDateTime(draft?.createdAt)}</p>
|
||||
</div>
|
||||
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
||||
{draft && (
|
||||
|
Loading…
x
Reference in New Issue
Block a user