diff --git a/app.rb b/app.rb
index 3152253..b1fc481 100644
--- a/app.rb
+++ b/app.rb
@@ -5,6 +5,8 @@ require 'yaml'
# Configure Sinatra
set :port, ENV['PORT'] || 4567
set :bind, '0.0.0.0'
+set :public_folder, 'public'
+set :views, 'views'
# Load responses from YAML file
RESPONSES = YAML.load_file('responses.yml')
@@ -215,453 +217,3 @@ get '/*' do
}
end
end
-
-__END__
-
-@@index
-
-
-
-
-
- No as a Service
-
-
-
-
-
No as a Service
-
The definitive multilingual API for negative responses
-
-
- Currently in: <%= language_name %>
- <% if language == 'no' %>🇳🇴 <% end %>
- <% if language == 'sv' %>🇸🇪 <% end %>
- <% if language == 'da' %>🇩🇰 <% end %>
- <% if language == 'is' %>🇮🇸 <% end %>
- <% if language == 'fi' %>🇫🇮 <% end %>
- <% if language == 'fo' %>🇫🇴 <% end %>
-
-
-
-
-
- <%= no_response %>
-
-
-
-
- Get Another No
-
-
-
- <% available_languages.each do |lang| %>
- >
- <%= lang[:name] %>
-
- <% end %>
-
-
-
-
-
API Usage
-
JSON API: GET /api/no or GET /api/no/:lang
-
Languages: GET /languages
-
URL Parameter: ?lang=<%= language %>
-
Accept-Language header supported
-
Health Check: GET /health
-
-
-
-
-
-
diff --git a/public/css/styles.css b/public/css/styles.css
new file mode 100644
index 0000000..5e81881
--- /dev/null
+++ b/public/css/styles.css
@@ -0,0 +1,342 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+}
+
+.container {
+ text-align: center;
+ max-width: 700px;
+ padding: 2rem;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border-radius: 20px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+h1 {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.subtitle {
+ font-size: 1.2rem;
+ margin-bottom: 1rem;
+ opacity: 0.9;
+}
+
+.language-info {
+ font-size: 1rem;
+ margin-bottom: 2rem;
+ padding: 0.5rem 1rem;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ display: inline-block;
+}
+
+.logo-container {
+ position: relative;
+ display: inline-block;
+ margin-bottom: 2rem;
+}
+
+.logo-container::before {
+ content: '';
+ position: absolute;
+ top: -20px;
+ left: -20px;
+ right: -20px;
+ bottom: -20px;
+ background: radial-gradient(
+ circle at 50% 50%,
+ transparent 60px,
+ rgba(255, 107, 107, 0.1) 80px,
+ transparent 120px
+ );
+ border-radius: 50%;
+ animation: pulseRing 3s ease-in-out infinite;
+ pointer-events: none;
+}
+
+.logo {
+ width: 140px;
+ height: 140px;
+ background: radial-gradient(circle at 30% 30%, #ff6b6b, #ee5a52, #dc3545);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 5rem;
+ text-shadow: 3px 3px 8px rgba(0, 0, 0, 0.6);
+ box-shadow:
+ 0 8px 32px rgba(255, 107, 107, 0.4),
+ inset 0 2px 10px rgba(255, 255, 255, 0.3),
+ inset 0 -2px 10px rgba(0, 0, 0, 0.2);
+ border: 3px solid rgba(255, 255, 255, 0.4);
+ position: relative;
+ animation: logoFloat 4s ease-in-out infinite;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ overflow: hidden;
+}
+
+.logo::before {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: -5px;
+ right: -5px;
+ bottom: -5px;
+ background: conic-gradient(
+ from 0deg,
+ #ff6b6b,
+ #ff8e8e,
+ #ffb3b3,
+ #ff6b6b,
+ #ee5a52,
+ #ff6b6b
+ );
+ border-radius: 50%;
+ z-index: -1;
+ animation: rotate 4s linear infinite;
+ opacity: 0.8;
+}
+
+.logo::after {
+ content: '';
+ position: absolute;
+ top: 15%;
+ left: 25%;
+ width: 30px;
+ height: 30px;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.8) 0%, transparent 70%);
+ border-radius: 50%;
+ filter: blur(8px);
+ animation: shimmer 3s ease-in-out infinite alternate;
+}
+
+.logo:hover {
+ transform: scale(1.1) rotate(5deg);
+ box-shadow:
+ 0 12px 40px rgba(255, 107, 107, 0.6),
+ inset 0 2px 15px rgba(255, 255, 255, 0.4),
+ inset 0 -2px 15px rgba(0, 0, 0, 0.3);
+ animation-play-state: paused;
+}
+
+.logo:active {
+ transform: scale(0.95);
+ animation: logoShake 0.5s ease-in-out;
+}
+
+.no-response {
+ font-size: 4rem;
+ font-weight: bold;
+ color: #ff6b6b;
+ text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5);
+ margin: 2rem 0;
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 15px;
+ border: 2px solid rgba(255, 107, 107, 0.3);
+ word-break: break-word;
+}
+
+.controls {
+ margin: 2rem 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ justify-content: center;
+ align-items: center;
+}
+
+.refresh-btn {
+ background: linear-gradient(45deg, #ff6b6b, #ff8e8e);
+ color: white;
+ border: none;
+ padding: 1rem 2rem;
+ font-size: 1.1rem;
+ border-radius: 50px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
+}
+
+.refresh-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
+}
+
+.language-selector {
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 25px;
+ font-size: 1rem;
+ cursor: pointer;
+}
+
+.language-selector option {
+ background: #444;
+ color: white;
+}
+
+.api-info {
+ margin-top: 2rem;
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 10px;
+ font-size: 0.9rem;
+}
+
+.api-info code {
+ background: rgba(0, 0, 0, 0.2);
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ font-family: 'Courier New', monospace;
+}
+
+.nordic-flag {
+ display: inline-block;
+ margin-left: 0.5rem;
+ font-size: 1.2em;
+}
+
+/* Animations */
+
+/* Floating animation */
+@keyframes logoFloat {
+ 0%, 100% {
+ transform: translateY(0px) rotate(0deg);
+ }
+ 25% {
+ transform: translateY(-8px) rotate(1deg);
+ }
+ 50% {
+ transform: translateY(-12px) rotate(0deg);
+ }
+ 75% {
+ transform: translateY(-6px) rotate(-1deg);
+ }
+}
+
+/* Enhanced rotation animation */
+@keyframes rotate {
+ from {
+ transform: rotate(0deg) scale(1);
+ }
+ 25% {
+ transform: rotate(90deg) scale(1.02);
+ }
+ 50% {
+ transform: rotate(180deg) scale(1);
+ }
+ 75% {
+ transform: rotate(270deg) scale(1.02);
+ }
+ to {
+ transform: rotate(360deg) scale(1);
+ }
+}
+
+/* Shimmer effect for the highlight */
+@keyframes shimmer {
+ 0% {
+ opacity: 0.3;
+ transform: scale(0.8);
+ }
+ 100% {
+ opacity: 0.8;
+ transform: scale(1.2);
+ }
+}
+
+/* Shake animation on click */
+@keyframes logoShake {
+ 0%, 100% { transform: translateX(0); }
+ 10% { transform: translateX(-5px) rotate(-2deg); }
+ 20% { transform: translateX(5px) rotate(2deg); }
+ 30% { transform: translateX(-5px) rotate(-1deg); }
+ 40% { transform: translateX(5px) rotate(1deg); }
+ 50% { transform: translateX(-3px); }
+ 60% { transform: translateX(3px); }
+ 70% { transform: translateX(-2px); }
+ 80% { transform: translateX(2px); }
+ 90% { transform: translateX(-1px); }
+}
+
+/* Pulsing ring animation */
+@keyframes pulseRing {
+ 0%, 100% {
+ transform: scale(0.8);
+ opacity: 0.3;
+ }
+ 50% {
+ transform: scale(1.1);
+ opacity: 0.1;
+ }
+}
+
+/* Sparkle animation */
+@keyframes sparkleFloat {
+ 0% {
+ opacity: 1;
+ transform: translateY(0) scale(0);
+ }
+ 50% {
+ opacity: 1;
+ transform: translateY(-30px) scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(-60px) scale(0) rotate(180deg);
+ }
+}
+
+/* Responsive Design */
+@media (max-width: 600px) {
+ .container {
+ margin: 1rem;
+ padding: 1.5rem;
+ }
+
+ h1 {
+ font-size: 2rem;
+ }
+
+ .logo {
+ width: 100px;
+ height: 100px;
+ font-size: 3.5rem;
+ }
+
+ .logo::after {
+ width: 20px;
+ height: 20px;
+ top: 20%;
+ left: 30%;
+ }
+
+ .no-response {
+ font-size: 2.5rem;
+ }
+
+ .controls {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+}
diff --git a/public/js/scripts.js b/public/js/scripts.js
new file mode 100644
index 0000000..b0c0e57
--- /dev/null
+++ b/public/js/scripts.js
@@ -0,0 +1,109 @@
+// Language selector functionality
+function changeLanguage(lang) {
+ const url = new URL(window.location);
+ url.searchParams.set('lang', lang);
+ window.location.href = url.toString();
+}
+
+// Enhanced logo interaction
+function logoClick() {
+ // Add a fun interaction when logo is clicked
+ const logo = document.querySelector('.logo');
+ logo.style.animationPlayState = 'running';
+
+ // Get a new "no" response when logo is clicked
+ setTimeout(() => {
+ location.reload();
+ }, 500);
+}
+
+// Sparkle effects on hover
+function createSparkles(element) {
+ const rect = element.getBoundingClientRect();
+ const sparkles = 5;
+
+ for (let i = 0; i < sparkles; i++) {
+ const sparkle = document.createElement('div');
+ sparkle.innerHTML = '✨';
+ sparkle.style.position = 'fixed';
+ sparkle.style.left = (rect.left + Math.random() * rect.width) + 'px';
+ sparkle.style.top = (rect.top + Math.random() * rect.height) + 'px';
+ sparkle.style.fontSize = (Math.random() * 20 + 10) + 'px';
+ sparkle.style.pointerEvents = 'none';
+ sparkle.style.zIndex = '1000';
+ sparkle.style.animation = 'sparkleFloat 2s ease-out forwards';
+
+ document.body.appendChild(sparkle);
+
+ setTimeout(() => {
+ sparkle.remove();
+ }, 2000);
+ }
+}
+
+// Initialize event listeners when DOM is loaded
+document.addEventListener('DOMContentLoaded', function() {
+ // Add sparkle effects on logo hover
+ const logo = document.querySelector('.logo');
+ if (logo) {
+ logo.addEventListener('mouseenter', function() {
+ createSparkles(this);
+ });
+ }
+
+ // Add keyboard shortcuts
+ document.addEventListener('keydown', function(event) {
+ // Press 'R' to refresh and get new response
+ if (event.key.toLowerCase() === 'r' && !event.ctrlKey && !event.metaKey) {
+ location.reload();
+ }
+
+ // Press 'L' to focus language selector
+ if (event.key.toLowerCase() === 'l' && !event.ctrlKey && !event.metaKey) {
+ const languageSelector = document.querySelector('.language-selector');
+ if (languageSelector) {
+ languageSelector.focus();
+ }
+ }
+ });
+
+ // Add subtle animations to elements on scroll (if needed in future)
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ entry.target.style.opacity = '1';
+ entry.target.style.transform = 'translateY(0)';
+ }
+ });
+ });
+
+ // Observe elements for animation (currently not needed but prepared for future use)
+ const animatedElements = document.querySelectorAll('.container > *');
+ animatedElements.forEach(el => {
+ observer.observe(el);
+ });
+});
+
+// Add some fun easter eggs
+let clickCount = 0;
+const logo = document.querySelector('.logo');
+
+if (logo) {
+ logo.addEventListener('click', function() {
+ clickCount++;
+
+ // Easter egg: Multiple rapid clicks
+ if (clickCount >= 5) {
+ this.style.animation = 'logoShake 0.5s ease-in-out, rotate 2s linear infinite';
+ setTimeout(() => {
+ this.style.animation = 'logoFloat 4s ease-in-out infinite';
+ }, 2000);
+ clickCount = 0;
+ }
+ });
+}
+
+// Reset click count after a delay
+setInterval(() => {
+ clickCount = 0;
+}, 3000);
diff --git a/views/index.erb b/views/index.erb
new file mode 100644
index 0000000..7ddb0f0
--- /dev/null
+++ b/views/index.erb
@@ -0,0 +1,60 @@
+
+
+
+
+
+ No as a Service
+
+
+
+
+
No as a Service
+
The definitive multilingual API for negative responses
+
+
+ Currently in: <%= language_name %>
+ <% if language == 'no' %>🇳🇴 <% end %>
+ <% if language == 'sv' %>🇸🇪 <% end %>
+ <% if language == 'da' %>🇩🇰 <% end %>
+ <% if language == 'is' %>🇮🇸 <% end %>
+ <% if language == 'fi' %>🇫🇮 <% end %>
+ <% if language == 'fo' %>🇫🇴 <% end %>
+
+
+
+
+
+ <%= no_response %>
+
+
+
+
+ Get Another No
+
+
+
+ <% available_languages.each do |lang| %>
+ >
+ <%= lang[:name] %>
+
+ <% end %>
+
+
+
+
+
API Usage
+
JSON API: GET /api/no or GET /api/no/:lang
+
Languages: GET /languages
+
URL Parameter: ?lang=<%= language %>
+
Accept-Language header supported
+
Health Check: GET /health
+
+
+
+
+
+