Jelajahi Sumber

feat: add GDPR Privacy Policy and Cookie Consent Banner

unknown 1 Minggu lalu
induk
melakukan
45a1f1a414

+ 75 - 0
backend/add_privacy_translations.py

@@ -0,0 +1,75 @@
+import json
+import os
+
+file_path = "d:\\radionica3d\\src\\locales\\translations.json"
+
+with open(file_path, 'r', encoding='utf-8') as f:
+    data = json.load(f)
+
+data["privacy"] = {
+    "title": {
+        "en": "Privacy Policy",
+        "ru": "Политика конфиденциальности",
+        "me": "Politika privatnosti"
+    },
+    "subtitle": {
+        "en": "Your data is safe with us. We respect your privacy and follow GDPR guidelines.",
+        "ru": "Ваши данные в безопасности. Мы уважаем вашу конфиденциальность и следуем принципам GDPR.",
+        "me": "Vaši podaci su bezbjedni. Poštujemo vašu privatnost i pratimo GDPR smjernice."
+    },
+    "sections": {
+        "collection": {
+            "title": {
+                "en": "Data Collection",
+                "ru": "Сбор данных",
+                "me": "Prikupljanje podataka"
+            },
+            "content": {
+                "en": "We collect your personal information (name, email, phone number, and address) exclusively for the purpose of processing and delivering your 3D printing orders. We do not use this data for marketing or other unsolicited purposes.",
+                "ru": "Мы собираем вашу личную информацию (имя, адрес электронной почты, номер телефона и адрес) исключительно в целях обработки и доставки ваших заказов на 3D-печать. Мы не используем эти данные для маркетинга или других нежелательных целей.",
+                "me": "Vaše lične podatke (ime, email, broj telefona i adresu) prikupljamo isključivo u svrhu obrade i dostave vaših porudžbina za 3D štampu. Ove podatke ne koristimo u marketinške ili druge nepredviđene svrhe."
+            }
+        },
+        "sharing": {
+            "title": {
+                "en": "Data Sharing",
+                "ru": "Передача данных",
+                "me": "Dijeljenje podataka"
+            },
+            "content": {
+                "en": "We do not share, sell, or disclose your personal data to any third parties, except where required by law (e.g., authorized government authorities).",
+                "ru": "Мы не передаем, не продаем и не раскрываем ваши личные данные третьим лицам, за исключением случаев, предусмотренных законом (например, уполномоченным государственным органам).",
+                "me": "Vaše lične podatke ne dijelimo, ne prodajemo i ne otkrivamo trećim licima, osim u slučajevima predviđenim zakonom (npr. ovlašćenim državnim organima)."
+            }
+        },
+        "retention": {
+            "title": {
+                "en": "Retention & Deletion",
+                "ru": "Хранение и удаление",
+                "me": "Zadržavanje i brisanje"
+            },
+            "content": {
+                "en": "Your data is stored securely. You have the right to request the deletion of your personal information at any time. Automatically, data may be deleted after a 12-month period of inactivity following order completion.",
+                "ru": "Ваши данные хранятся в безопасности. Вы имеете право запросить удаление вашей личной информации в любое время. Данные могут быть автоматически удалены по истечении 12-месячного периода бездействия после завершения заказа.",
+                "me": "Vaši podaci se čuvaju na siguran način. Imate pravo da zatražite brisanje svojih ličnih podataka u bilo kom trenutku. Podaci se automatski brišu nakon perioda od 12 mjeseci neaktivnosti nakon završetka porudžbine."
+            }
+        },
+        "rights": {
+            "title": {
+                "en": "GDPR Compliance",
+                "ru": "Соответствие GDPR",
+                "me": "Usklađenost sa GDPR"
+            },
+            "content": {
+                "en": "Under GDPR, you have the right to access, rectify, or erase your data, as well as the right to data portability. Contact us at hello@radionica3d.me for any privacy-related requests.",
+                "ru": "В соответствии с GDPR у вас есть право на доступ к своим данным, их исправление или удаление, а также право на переносимость данных. Свяжитесь с нами по адресу hello@radionica3d.me по любым вопросам, связанным с конфиденциальностью.",
+                "me": "Prema GDPR regulativi, imate pravo na pristup, ispravku ili brisanje svojih podataka, kao i pravo na prenosivost podataka. Kontaktirajte nas na hello@radionica3d.me za sve upite u vezi sa privatnošću."
+            }
+        }
+    }
+}
+
+with open(file_path, 'w', encoding='utf-8') as f:
+    json.dump(data, f, ensure_ascii=False, indent=2)
+
+print("Translations updated successfully.")

+ 6 - 0
backend/main.py

@@ -1,4 +1,5 @@
 from fastapi import FastAPI, HTTPException, Request
+from fastapi.staticfiles import StaticFiles
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.exceptions import RequestValidationError
 from fastapi.responses import JSONResponse
@@ -55,6 +56,11 @@ app.include_router(portfolio.router)
 app.include_router(files.router)
 app.include_router(chat.router)
 
+# Mount Static Files
+if not os.path.exists("uploads"):
+    os.makedirs("uploads")
+app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
+
 if __name__ == "__main__":
     import uvicorn
     uvicorn.run(app, host="0.0.0.0", port=8000)

+ 46 - 0
backend/seed_portfolio.py

@@ -0,0 +1,46 @@
+import db
+
+def seed():
+    # Disable foreign key checks to make it easier to seed orders without users
+    db.execute_commit("SET FOREIGN_KEY_CHECKS = 0;")
+    
+    # 1. Clear existing portfolio data for clean test
+    db.execute_commit("DELETE FROM order_photos;")
+    
+    # 2. Insert Orders with consent
+    orders_data = [
+        ('Nikola', 'Tesla', '+38267123456', 'nikola@tesla.me', 'Podgorica, Montenegro', 'SLA Resin', True),
+        ('John', 'Doe', '+38267000111', 'john@gmail.com', 'Budva, Montenegro', 'PLA Plastic', True),
+        ('Alice', 'Smith', '+38268333444', 'alice@matrix.me', 'Kotor, Montenegro', 'Resin', True),
+        ('Bob', 'Ross', '+38269555666', 'bob@art.me', 'Herceg Novi, Montenegro', 'ABS Plastic', True),
+        ('Tony', 'Stark', '+38267999888', 'tony@stark.me', 'Tivat, Montenegro', 'Engineering Plastic', True),
+    ]
+    
+    order_ids = []
+    for o in orders_data:
+        qid = db.execute_commit(
+            "INSERT INTO orders (first_name, last_name, phone, email, shipping_address, material_name, allow_portfolio, status) VALUES (%s, %s, %s, %s, %s, %s, %s, 'completed')",
+            o
+        )
+        order_ids.append(qid)
+        
+    # 3. Insert Photos
+    photos = [
+        (order_ids[0], 'uploads/portfolio_gear.png', True),
+        (order_ids[1], 'uploads/portfolio_arch.png', True),
+        (order_ids[2], 'uploads/portfolio_voronoi.png', True),
+        (order_ids[3], 'uploads/portfolio_prosthetic.png', True),
+        (order_ids[4], 'uploads/portfolio_minifigs.png', True),
+    ]
+    
+    for p in photos:
+        db.execute_commit(
+            "INSERT INTO order_photos (order_id, file_path, is_public) VALUES (%s, %s, %s)",
+            p
+        )
+    
+    db.execute_commit("SET FOREIGN_KEY_CHECKS = 1;")
+    print("Portfolio seeded successfully with 5 items.")
+
+if __name__ == "__main__":
+    seed()

+ 2 - 0
src/App.vue

@@ -1,6 +1,7 @@
 <template>
   <RouterView />
   <Toaster />
+  <CookieBanner />
   <CompleteProfileModal
     v-if="authStore.user"
     :is-open="authStore.showCompleteProfile"
@@ -13,6 +14,7 @@
 import { Toaster } from "vue-sonner";
 import { useAuthStore } from "@/stores/auth";
 import CompleteProfileModal from "@/components/CompleteProfileModal.vue";
+import CookieBanner from "@/components/CookieBanner.vue";
 
 const authStore = useAuthStore();
 authStore.init();

+ 43 - 0
src/components/CookieBanner.vue

@@ -0,0 +1,43 @@
+<template>
+  <div v-if="isVisible" class="fixed bottom-0 left-0 right-0 z-50 p-4 animate-in slide-in-from-bottom-4 duration-500">
+    <div class="max-w-4xl mx-auto bg-foreground text-background p-4 sm:p-6 rounded-2xl shadow-2xl flex flex-col sm:flex-row items-center gap-4 sm:gap-6">
+      <div class="flex-1 text-sm font-medium text-background/90 text-center sm:text-left">
+        {{ t("cookies.message") }}
+      </div>
+      <div class="flex flex-shrink-0 gap-3 w-full sm:w-auto">
+        <button @click="leave" class="flex-1 sm:flex-none px-4 py-2 bg-background/20 hover:bg-background/30 text-background rounded-full font-bold text-sm transition-colors whitespace-nowrap">
+          {{ t("cookies.leave") }}
+        </button>
+        <button @click="accept" class="flex-1 sm:flex-none px-6 py-2 bg-primary text-primary-foreground rounded-full font-bold text-sm transition-colors whitespace-nowrap">
+          {{ t("cookies.accept") }}
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+const isVisible = ref(false);
+
+onMounted(() => {
+  const consent = localStorage.getItem('cookie_consent');
+  if (!consent) {
+    isVisible.value = true;
+  }
+});
+
+const accept = () => {
+  localStorage.setItem('cookie_consent', 'accepted');
+  isVisible.value = false;
+};
+
+const leave = () => {
+  // If user clicks leave, we can redirect them elsewhere
+  // Or just close the window
+  window.history.length > 1 ? window.history.back() : (window.location.href = "https://www.google.com");
+};
+</script>

+ 1 - 1
src/components/Footer.vue

@@ -48,7 +48,7 @@
       <div class="pt-6 border-t border-black/[0.04] flex flex-col sm:flex-row justify-between items-center gap-4">
         <p class="text-[10px] font-bold text-foreground/30 uppercase tracking-widest">© 2024 Radionica 3D. {{ t("footer.allRightsReserved") }}</p>
         <div class="flex gap-4">
-          <a href="#" class="text-[10px] font-bold text-foreground/30 hover:text-primary transition-colors uppercase tracking-widest">{{ t("footer.privacy") }}</a>
+          <router-link to="/privacy" class="text-[10px] font-bold text-foreground/30 hover:text-primary transition-colors uppercase tracking-widest">{{ t("footer.privacy") }}</router-link>
           <a href="#" class="text-[10px] font-bold text-foreground/30 hover:text-primary transition-colors uppercase tracking-widest">{{ t("footer.terms") }}</a>
         </div>
       </div>

+ 78 - 0
src/locales/translations.json

@@ -667,5 +667,83 @@
       "me": "vjeruju",
       "ru": "доверяем"
     }
+  },
+  "privacy": {
+    "title": {
+      "en": "Privacy Policy",
+      "ru": "Политика конфиденциальности",
+      "me": "Politika privatnosti"
+    },
+    "subtitle": {
+      "en": "Your data is safe with us. We respect your privacy and follow GDPR guidelines.",
+      "ru": "Ваши данные в безопасности. Мы уважаем вашу конфиденциальность и следуем принципам GDPR.",
+      "me": "Vaši podaci su bezbjedni. Poštujemo vašu privatnost i pratimo GDPR smjernice."
+    },
+    "sections": {
+      "collection": {
+        "title": {
+          "en": "Data Collection",
+          "ru": "Сбор данных",
+          "me": "Prikupljanje podataka"
+        },
+        "content": {
+          "en": "We collect your personal information (name, email, phone number, and address) exclusively for the purpose of processing and delivering your 3D printing orders. We do not use this data for marketing or other unsolicited purposes.",
+          "ru": "Мы собираем вашу личную информацию (имя, адрес электронной почты, номер телефона и адрес) исключительно в целях обработки и доставки ваших заказов на 3D-печать. Мы не используем эти данные для маркетинга или других нежелательных целей.",
+          "me": "Vaše lične podatke (ime, email, broj telefona i adresu) prikupljamo isključivo u svrhu obrade i dostave vaših porudžbina za 3D štampu. Ove podatke ne koristimo u marketinške ili druge nepredviđene svrhe."
+        }
+      },
+      "sharing": {
+        "title": {
+          "en": "Data Sharing",
+          "ru": "Передача данных",
+          "me": "Dijeljenje podataka"
+        },
+        "content": {
+          "en": "We do not share, sell, or disclose your personal data to any third parties, except where required by law (e.g., authorized government authorities).",
+          "ru": "Мы не передаем, не продаем и не раскрываем ваши личные данные третьим лицам, за исключением случаев, предусмотренных законом (например, уполномоченным государственным органам).",
+          "me": "Vaše lične podatke ne dijelimo, ne prodajemo i ne otkrivamo trećim licima, osim u slučajevima predviđenim zakonom (npr. ovlašćenim državnim organima)."
+        }
+      },
+      "retention": {
+        "title": {
+          "en": "Retention & Deletion",
+          "ru": "Хранение и удаление",
+          "me": "Zadržavanje i brisanje"
+        },
+        "content": {
+          "en": "Your data is stored securely. You have the right to request the deletion of your personal information at any time. Automatically, data may be deleted after a 12-month period of inactivity following order completion.",
+          "ru": "Ваши данные хранятся в безопасности. Вы имеете право запросить удаление вашей личной информации в любое время. Данные могут быть автоматически удалены по истечении 12-месячного периода бездействия после завершения заказа.",
+          "me": "Vaši podaci se čuvaju na siguran način. Imate pravo da zatražite brisanje svojih ličnih podataka u bilo kom trenutku. Podaci se automatski brišu nakon perioda od 12 mjeseci neaktivnosti nakon završetka porudžbine."
+        }
+      },
+      "rights": {
+        "title": {
+          "en": "GDPR Compliance",
+          "ru": "Соответствие GDPR",
+          "me": "Usklađenost sa GDPR"
+        },
+        "content": {
+          "en": "Under GDPR, you have the right to access, rectify, or erase your data, as well as the right to data portability. Contact us at hello@radionica3d.me for any privacy-related requests.",
+          "ru": "В соответствии с GDPR у вас есть право на доступ к своим данным, их исправление или удаление, а также право на переносимость данных. Свяжитесь с нами по адресу hello@radionica3d.me по любым вопросам, связанным с конфиденциальностью.",
+          "me": "Prema GDPR regulativi, imate pravo na pristup, ispravku ili brisanje svojih podataka, kao i pravo na prenosivost podataka. Kontaktirajte nas na hello@radionica3d.me za sve upite u vezi sa privatnošću."
+        }
+    }
+  },
+  "cookies": {
+    "message": {
+      "en": "This site uses cookies to improve your experience and analyze traffic.",
+      "ru": "Данный сайт использует файлы cookie для улучшения пользовательского опыта.",
+      "me": "Ovaj sajt koristi kolačiće za pružanje boljeg korisničkog iskustva."
+    },
+    "accept": {
+      "en": "Accept",
+      "ru": "Принять",
+      "me": "Prihvati"
+    },
+    "leave": {
+      "en": "Leave",
+      "ru": "Уйти",
+      "me": "Napusti"
+    }
   }
 }

+ 37 - 0
src/pages/Privacy.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="min-h-screen bg-background text-foreground pt-32 pb-24">
+    <div class="container mx-auto px-4 max-w-3xl">
+      <h1 class="font-display text-4xl md:text-5xl font-extrabold tracking-tight mb-6">
+        {{ t("privacy.title") }}
+      </h1>
+      <p class="text-xl text-foreground/60 font-medium mb-16">
+        {{ t("privacy.subtitle") }}
+      </p>
+
+      <div class="space-y-12">
+        <section v-for="section in sections" :key="section" class="space-y-4">
+          <h2 class="font-display text-2xl font-bold tracking-tight text-foreground">
+            {{ t(`privacy.sections.${section}.title`) }}
+          </h2>
+          <p class="text-foreground/70 leading-relaxed font-medium">
+            {{ t(`privacy.sections.${section}.content`) }}
+          </p>
+        </section>
+      </div>
+      
+      <!-- Back to Home -->
+      <div class="mt-16 pt-8 border-t border-black/[0.1]">
+        <router-link to="/" class="inline-flex items-center gap-2 text-sm font-bold text-foreground hover:text-primary transition-colors">
+          &larr; {{ t("auth.back") }}
+        </router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
+const sections = ["collection", "sharing", "retention", "rights"];
+</script>

+ 1 - 0
src/router/index.ts

@@ -8,6 +8,7 @@ const router = createRouter({
     { path: "/orders",    component: () => import("@/pages/Orders.vue") },
     { path: "/portfolio", component: () => import("@/pages/Portfolio.vue") },
     { path: "/admin",     component: () => import("@/pages/Admin.vue") },
+    { path: "/privacy",   component: () => import("@/pages/Privacy.vue") },
     { path: "/:pathMatch(.*)*", component: () => import("@/pages/NotFound.vue") },
   ],
   scrollBehavior(to) {

+ 57 - 0
src/views/PrivacyPolicy.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="min-h-screen bg-white pt-24 pb-16">
+    <div class="container mx-auto px-4 max-w-3xl">
+      <div class="mb-12 animate-slide-up">
+        <h1 class="font-display text-4xl sm:text-5xl font-extrabold mb-4 tracking-tight">
+          {{ t("privacy.title") }}
+        </h1>
+        <p class="text-foreground/60 text-lg font-medium leading-relaxed">
+          {{ t("privacy.subtitle") }}
+        </p>
+      </div>
+
+      <div class="space-y-10">
+        <section 
+          v-for="(section, idx) in tm('privacy.sections')" 
+          :key="idx"
+          class="animate-slide-up"
+          :style="{ animationDelay: `${idx * 100}ms` }"
+        >
+          <div class="flex items-start gap-4">
+            <div class="w-10 h-10 bg-primary/5 rounded-xl flex items-center justify-center flex-shrink-0 mt-1">
+              <span class="text-primary font-display font-bold">0{{ idx + 1 }}</span>
+            </div>
+            <div class="space-y-2">
+              <h2 class="font-display text-xl font-bold tracking-tight text-foreground">
+                {{ rt(section.title) }}
+              </h2>
+              <p class="text-foreground/50 text-base leading-relaxed font-medium">
+                {{ rt(section.content) }}
+              </p>
+            </div>
+          </div>
+        </section>
+      </div>
+
+      <div class="mt-16 p-8 rounded-[2rem] bg-secondary/30 animate-fade-in">
+        <div class="flex flex-col sm:flex-row items-center justify-between gap-6">
+          <div class="space-y-1">
+            <h3 class="font-display text-lg font-bold">{{ t("footer.contact") }}</h3>
+            <p class="text-foreground/40 text-sm font-medium">We respond to all privacy requests within 48 hours.</p>
+          </div>
+          <a href="mailto:hello@radionica3d.me" class="inline-flex items-center gap-2 px-6 py-3 bg-white border border-black/[0.05] rounded-2xl font-bold text-sm hover:shadow-md transition-all active:scale-95">
+            <Mail class="w-4 h-4 text-primary" />
+            hello@radionica3d.me
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from "vue-i18n";
+import { Mail } from "lucide-vue-next";
+
+const { t, tm, rt } = useI18n();
+</script>