Commit f5b02dc
Changed files (8)
cmd
web
docs
ui
html
layouts
pages
static
cmd/web/main.go
@@ -10,6 +10,16 @@ func main() {
// URL patterns and their corresponding handlers.
mux := http.NewServeMux()
+ // Create a file server which serves files out of the "./ui/static" directory.
+ // Note that the path given to the http.Dir function is relative to the project
+ // directory root.
+ fileServer := http.FileServer(http.Dir("./ui/static/"))
+
+ // Use the mux.Handle() function to register the file server as the handler for
+ // all URL paths that start with "/static/". For matching paths, we strip the
+ // "/static" prefix before the request reaches the file server.
+ mux.Handle("GET /static/", http.StripPrefix("/static", fileServer))
+
// Register handlers for buylater.email routes
mux.HandleFunc("GET /{$}", home) // Exact match for home page
mux.HandleFunc("GET /submit", submitForm) // Display email submission form
docs/project_plan.md
@@ -16,7 +16,7 @@ Phase 1 lays the groundwork by methodically working through the Let's Go book, c
| 1.6 | Custom Response Headers and Status Codes | Completed | Small | 2.6 | [task_1.6.md](todo/task_1.6.md) |
| 1.7 | Project Structure and Organization | Completed | Medium | 2.7 | [task_1.7.md](todo/task_1.7.md) |
| 1.8 | HTML Templating and Inheritance | Completed | Medium | 2.8 | [task_1.8.md](todo/task_1.8.md) |
-| 1.9 | Serving Static Files | In Progress | Small | 2.9 | [task_1.9.md](todo/task_1.9.md) |
+| 1.9 | Serving Static Files | Completed | Small | 2.9 | [task_1.9.md](todo/task_1.9.md) |
## Status Legend
- **Completed** - Implemented and verified
ui/html/layouts/base.tmpl
@@ -5,95 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - buylater.email</title>
- <style>
- * {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- }
-
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
- line-height: 1.6;
- color: #333;
- background-color: #f8f9fa;
- }
-
- .container {
- max-width: 800px;
- margin: 0 auto;
- padding: 0 20px;
- }
-
- header {
- background-color: #fff;
- border-bottom: 1px solid #e9ecef;
- padding: 1rem 0;
- }
-
- .header-content {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .logo {
- font-size: 1.5rem;
- font-weight: 600;
- color: #2c3e50;
- text-decoration: none;
- }
-
- nav a {
- text-decoration: none;
- color: #6c757d;
- margin-left: 1.5rem;
- font-weight: 500;
- }
-
- nav a:hover {
- color: #2c3e50;
- }
-
- main {
- padding: 2rem 0;
- min-height: calc(100vh - 160px);
- }
-
- footer {
- background-color: #fff;
- border-top: 1px solid #e9ecef;
- padding: 1.5rem 0;
- margin-top: 2rem;
- color: #6c757d;
- font-size: 0.9rem;
- }
-
- .footer-content {
- text-align: center;
- }
-
- .tagline {
- font-style: italic;
- margin-top: 0.5rem;
- }
-
- @media (max-width: 768px) {
- .container {
- padding: 0 15px;
- }
-
- .header-content {
- flex-direction: column;
- gap: 1rem;
- }
-
- nav a {
- margin-left: 1rem;
- margin-right: 1rem;
- }
- }
- </style>
+ <link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg">
+ <link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<header>
@@ -123,6 +36,8 @@
</div>
</div>
</footer>
+
+ <script src="/static/js/main.js"></script>
</body>
</html>
{{end}}
\ No newline at end of file
ui/html/pages/home.tmpl
@@ -37,121 +37,4 @@
</blockquote>
</div>
-<style>
-.hero {
- text-align: center;
- padding: 3rem 0;
-}
-
-.hero h1 {
- font-size: 2.5rem;
- font-weight: 700;
- color: #2c3e50;
- margin-bottom: 1rem;
-}
-
-.hero-subtitle {
- font-size: 1.2rem;
- color: #6c757d;
- margin-bottom: 2rem;
- max-width: 600px;
- margin-left: auto;
- margin-right: auto;
-}
-
-.cta-section {
- margin: 2rem 0;
-}
-
-.cta-button {
- display: inline-block;
- background-color: #2c3e50;
- color: white;
- padding: 1rem 2rem;
- text-decoration: none;
- border-radius: 8px;
- font-weight: 600;
- font-size: 1.1rem;
- transition: background-color 0.2s;
-}
-
-.cta-button:hover {
- background-color: #34495e;
-}
-
-.cta-description {
- margin-top: 0.5rem;
- color: #6c757d;
- font-size: 0.9rem;
-}
-
-.features {
- margin: 4rem 0;
-}
-
-.feature-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 2rem;
- margin-top: 2rem;
-}
-
-.feature {
- background: white;
- padding: 1.5rem;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-}
-
-.feature h3 {
- color: #2c3e50;
- margin-bottom: 0.5rem;
-}
-
-.philosophy {
- background: white;
- padding: 2rem;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- margin: 3rem 0;
-}
-
-.philosophy h2 {
- color: #2c3e50;
- margin-bottom: 1rem;
-}
-
-.philosophy p {
- margin-bottom: 1.5rem;
- line-height: 1.7;
-}
-
-.philosophy blockquote {
- font-style: italic;
- font-size: 1.1rem;
- color: #6c757d;
- border-left: 4px solid #2c3e50;
- padding-left: 1rem;
- margin: 1rem 0;
-}
-
-@media (max-width: 768px) {
- .hero h1 {
- font-size: 2rem;
- }
-
- .hero-subtitle {
- font-size: 1.1rem;
- }
-
- .feature-grid {
- grid-template-columns: 1fr;
- gap: 1rem;
- }
-
- .philosophy {
- padding: 1.5rem;
- }
-}
-</style>
{{end}}
\ No newline at end of file
ui/html/pages/submit.tmpl
@@ -47,120 +47,4 @@
</div>
</div>
-<style>
-.submit-page {
- max-width: 600px;
- margin: 0 auto;
-}
-
-.submit-header {
- text-align: center;
- margin-bottom: 3rem;
-}
-
-.submit-header h1 {
- color: #2c3e50;
- margin-bottom: 0.5rem;
-}
-
-.submit-header p {
- color: #6c757d;
- font-size: 1.1rem;
-}
-
-.submit-form-container {
- display: grid;
- gap: 3rem;
-}
-
-.submit-form {
- background: white;
- padding: 2rem;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-}
-
-.form-group {
- margin-bottom: 1.5rem;
-}
-
-.form-group label {
- display: block;
- font-weight: 600;
- color: #2c3e50;
- margin-bottom: 0.5rem;
-}
-
-.form-group input,
-.form-group select {
- width: 100%;
- padding: 0.75rem;
- border: 2px solid #e9ecef;
- border-radius: 4px;
- font-size: 1rem;
- transition: border-color 0.2s;
-}
-
-.form-group input:focus,
-.form-group select:focus {
- outline: none;
- border-color: #2c3e50;
-}
-
-.form-group small {
- display: block;
- margin-top: 0.25rem;
- color: #6c757d;
- font-size: 0.9rem;
-}
-
-.submit-button {
- width: 100%;
- background-color: #2c3e50;
- color: white;
- padding: 1rem 2rem;
- border: none;
- border-radius: 8px;
- font-weight: 600;
- font-size: 1.1rem;
- cursor: pointer;
- transition: background-color 0.2s;
-}
-
-.submit-button:hover {
- background-color: #34495e;
-}
-
-.submit-info {
- background: white;
- padding: 2rem;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-}
-
-.submit-info h3 {
- color: #2c3e50;
- margin-bottom: 1rem;
-}
-
-.submit-info ol {
- padding-left: 1.2rem;
-}
-
-.submit-info li {
- margin-bottom: 0.5rem;
- line-height: 1.6;
-}
-
-@media (max-width: 768px) {
- .submit-form,
- .submit-info {
- padding: 1.5rem;
- }
-
- .submit-form-container {
- gap: 2rem;
- }
-}
-</style>
{{end}}
\ No newline at end of file
ui/static/css/main.css
@@ -0,0 +1,321 @@
+/* Reset and Base Styles */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ background-color: #f8f9fa;
+}
+
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+/* Header Styles */
+header {
+ background-color: #fff;
+ border-bottom: 1px solid #e9ecef;
+ padding: 1rem 0;
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.logo {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+ text-decoration: none;
+}
+
+nav a {
+ text-decoration: none;
+ color: #6c757d;
+ margin-left: 1.5rem;
+ font-weight: 500;
+}
+
+nav a:hover {
+ color: #2c3e50;
+}
+
+/* Main Content */
+main {
+ padding: 2rem 0;
+ min-height: calc(100vh - 160px);
+}
+
+/* Footer */
+footer {
+ background-color: #fff;
+ border-top: 1px solid #e9ecef;
+ padding: 1.5rem 0;
+ margin-top: 2rem;
+ color: #6c757d;
+ font-size: 0.9rem;
+}
+
+.footer-content {
+ text-align: center;
+}
+
+.tagline {
+ font-style: italic;
+ margin-top: 0.5rem;
+}
+
+/* Home Page Styles */
+.hero {
+ text-align: center;
+ padding: 3rem 0;
+}
+
+.hero h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ color: #2c3e50;
+ margin-bottom: 1rem;
+}
+
+.hero-subtitle {
+ font-size: 1.2rem;
+ color: #6c757d;
+ margin-bottom: 2rem;
+ max-width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.cta-section {
+ margin: 2rem 0;
+}
+
+.cta-button {
+ display: inline-block;
+ background-color: #2c3e50;
+ color: white;
+ padding: 1rem 2rem;
+ text-decoration: none;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 1.1rem;
+ transition: background-color 0.2s;
+}
+
+.cta-button:hover {
+ background-color: #34495e;
+}
+
+.cta-description {
+ margin-top: 0.5rem;
+ color: #6c757d;
+ font-size: 0.9rem;
+}
+
+.features {
+ margin: 4rem 0;
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+ margin-top: 2rem;
+}
+
+.feature {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.feature h3 {
+ color: #2c3e50;
+ margin-bottom: 0.5rem;
+}
+
+.philosophy {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ margin: 3rem 0;
+}
+
+.philosophy h2 {
+ color: #2c3e50;
+ margin-bottom: 1rem;
+}
+
+.philosophy p {
+ margin-bottom: 1.5rem;
+ line-height: 1.7;
+}
+
+.philosophy blockquote {
+ font-style: italic;
+ font-size: 1.1rem;
+ color: #6c757d;
+ border-left: 4px solid #2c3e50;
+ padding-left: 1rem;
+ margin: 1rem 0;
+}
+
+/* Submit Page Styles */
+.submit-page {
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.submit-header {
+ text-align: center;
+ margin-bottom: 3rem;
+}
+
+.submit-header h1 {
+ color: #2c3e50;
+ margin-bottom: 0.5rem;
+}
+
+.submit-header p {
+ color: #6c757d;
+ font-size: 1.1rem;
+}
+
+.submit-form-container {
+ display: grid;
+ gap: 3rem;
+}
+
+.submit-form {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-group label {
+ display: block;
+ font-weight: 600;
+ color: #2c3e50;
+ margin-bottom: 0.5rem;
+}
+
+.form-group input,
+.form-group select {
+ width: 100%;
+ padding: 0.75rem;
+ border: 2px solid #e9ecef;
+ border-radius: 4px;
+ font-size: 1rem;
+ transition: border-color 0.2s;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: #2c3e50;
+}
+
+.form-group small {
+ display: block;
+ margin-top: 0.25rem;
+ color: #6c757d;
+ font-size: 0.9rem;
+}
+
+.submit-button {
+ width: 100%;
+ background-color: #2c3e50;
+ color: white;
+ padding: 1rem 2rem;
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 1.1rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.submit-button:hover {
+ background-color: #34495e;
+}
+
+.submit-info {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.submit-info h3 {
+ color: #2c3e50;
+ margin-bottom: 1rem;
+}
+
+.submit-info ol {
+ padding-left: 1.2rem;
+}
+
+.submit-info li {
+ margin-bottom: 0.5rem;
+ line-height: 1.6;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .container {
+ padding: 0 15px;
+ }
+
+ .header-content {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ nav a {
+ margin-left: 1rem;
+ margin-right: 1rem;
+ }
+
+ .hero h1 {
+ font-size: 2rem;
+ }
+
+ .hero-subtitle {
+ font-size: 1.1rem;
+ }
+
+ .feature-grid {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .philosophy {
+ padding: 1.5rem;
+ }
+
+ .submit-form,
+ .submit-info {
+ padding: 1.5rem;
+ }
+
+ .submit-form-container {
+ gap: 2rem;
+ }
+}
\ No newline at end of file
ui/static/img/favicon.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
+ <rect width="32" height="32" fill="#2c3e50"/>
+ <text x="16" y="20" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="white">B</text>
+</svg>
\ No newline at end of file
ui/static/js/main.js
@@ -0,0 +1,163 @@
+// Main JavaScript for buylater.email
+// Provides form validation and UX enhancements
+
+document.addEventListener('DOMContentLoaded', function() {
+ // Form validation for submit page
+ const submitForm = document.querySelector('.submit-form');
+
+ if (submitForm) {
+ setupFormValidation(submitForm);
+ }
+
+ // Add smooth scrolling for anchor links
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function (e) {
+ e.preventDefault();
+ const target = document.querySelector(this.getAttribute('href'));
+ if (target) {
+ target.scrollIntoView({
+ behavior: 'smooth'
+ });
+ }
+ });
+ });
+});
+
+function setupFormValidation(form) {
+ const emailInput = form.querySelector('#email');
+ const urlInput = form.querySelector('#url');
+ const submitButton = form.querySelector('.submit-button');
+
+ // Real-time validation for email
+ if (emailInput) {
+ emailInput.addEventListener('blur', function() {
+ validateEmail(this);
+ });
+
+ emailInput.addEventListener('input', function() {
+ clearValidationError(this);
+ });
+ }
+
+ // Real-time validation for URL
+ if (urlInput) {
+ urlInput.addEventListener('blur', function() {
+ validateUrl(this);
+ });
+
+ urlInput.addEventListener('input', function() {
+ clearValidationError(this);
+ });
+ }
+
+ // Form submission validation
+ form.addEventListener('submit', function(e) {
+ let isValid = true;
+
+ if (emailInput && !validateEmail(emailInput)) {
+ isValid = false;
+ }
+
+ if (urlInput && !validateUrl(urlInput)) {
+ isValid = false;
+ }
+
+ if (!isValid) {
+ e.preventDefault();
+ showFormError('Please fix the errors above before submitting.');
+ } else {
+ // Show loading state
+ submitButton.textContent = 'Scheduling...';
+ submitButton.disabled = true;
+ }
+ });
+}
+
+function validateEmail(input) {
+ const email = input.value.trim();
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ if (!email) {
+ showFieldError(input, 'Email address is required.');
+ return false;
+ }
+
+ if (!emailRegex.test(email)) {
+ showFieldError(input, 'Please enter a valid email address.');
+ return false;
+ }
+
+ clearValidationError(input);
+ return true;
+}
+
+function validateUrl(input) {
+ const url = input.value.trim();
+
+ if (!url) {
+ showFieldError(input, 'Product URL is required.');
+ return false;
+ }
+
+ try {
+ const urlObj = new URL(url);
+ if (!['http:', 'https:'].includes(urlObj.protocol)) {
+ throw new Error('Invalid protocol');
+ }
+ } catch (e) {
+ showFieldError(input, 'Please enter a valid URL (including http:// or https://).');
+ return false;
+ }
+
+ clearValidationError(input);
+ return true;
+}
+
+function showFieldError(input, message) {
+ clearValidationError(input);
+
+ const errorDiv = document.createElement('div');
+ errorDiv.className = 'field-error';
+ errorDiv.textContent = message;
+ errorDiv.style.color = '#dc3545';
+ errorDiv.style.fontSize = '0.875rem';
+ errorDiv.style.marginTop = '0.25rem';
+
+ input.style.borderColor = '#dc3545';
+ input.parentNode.appendChild(errorDiv);
+}
+
+function clearValidationError(input) {
+ const existingError = input.parentNode.querySelector('.field-error');
+ if (existingError) {
+ existingError.remove();
+ }
+
+ const existingFormError = document.querySelector('.form-error');
+ if (existingFormError) {
+ existingFormError.remove();
+ }
+
+ input.style.borderColor = '#e9ecef';
+}
+
+function showFormError(message) {
+ const existingError = document.querySelector('.form-error');
+ if (existingError) {
+ existingError.remove();
+ }
+
+ const errorDiv = document.createElement('div');
+ errorDiv.className = 'form-error';
+ errorDiv.textContent = message;
+ errorDiv.style.color = '#dc3545';
+ errorDiv.style.backgroundColor = '#f8d7da';
+ errorDiv.style.border = '1px solid #f5c6cb';
+ errorDiv.style.borderRadius = '4px';
+ errorDiv.style.padding = '0.75rem';
+ errorDiv.style.marginBottom = '1rem';
+ errorDiv.style.fontSize = '0.875rem';
+
+ const form = document.querySelector('.submit-form');
+ form.insertBefore(errorDiv, form.firstChild);
+}
\ No newline at end of file