Improve translation system, add fully responsive admin panel, and various other fixes.
This commit is contained in:
@@ -16,6 +16,12 @@ export const appRouter = router({
|
||||
healthCheck: publicProcedure.query(() => {
|
||||
return "OK";
|
||||
}),
|
||||
checkAuthStatus: publicProcedure.query(({ ctx }) => {
|
||||
return {
|
||||
isLoggedIn: !!ctx.session,
|
||||
user: ctx.session?.user || null
|
||||
};
|
||||
}),
|
||||
privateData: protectedProcedure.query(({ ctx }) => {
|
||||
return {
|
||||
message: "This is private",
|
||||
|
||||
@@ -1,578 +0,0 @@
|
||||
{
|
||||
"admin": {
|
||||
"dashboard": {
|
||||
"title": "Admin Panel",
|
||||
"description": "Welcome to the admin panel. Choose a section you want to edit."
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Homepage",
|
||||
"description": "Edit homepage content",
|
||||
"welcomeText": "Welcome Text",
|
||||
"welcomePlaceholder": "Enter welcome text...",
|
||||
"specializationText": "Specialization",
|
||||
"specializationPlaceholder": "Enter specialization text...",
|
||||
"aboutMeText": "About Me",
|
||||
"aboutMePlaceholder": "Enter about me text...",
|
||||
"updateSuccess": "Homepage updated successfully"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"addNew": "Add skill",
|
||||
"addNewTitle": "Add new skill",
|
||||
"editTitle": "Edit skill",
|
||||
"namePl": "Name (PL)",
|
||||
"nameEn": "Name (EN)",
|
||||
"categoryPl": "Category (PL)",
|
||||
"categoryEn": "Category (EN)",
|
||||
"category": "Category",
|
||||
"selectCategory": "Select category",
|
||||
"categoriesTitle": "Skill categories",
|
||||
"addNewCategory": "Add category",
|
||||
"addNewCategoryTitle": "Add new category",
|
||||
"editCategoryTitle": "Edit category",
|
||||
"deleteCategoryTitle": "Delete category",
|
||||
"deleteCategoryConfirm": "Are you sure you want to delete this category? Skills assigned to this category will not be assigned to any category.",
|
||||
"icon": "Icon",
|
||||
"iconName": "Icon name",
|
||||
"iconProvider": "Icon provider",
|
||||
"order": "Order",
|
||||
"active": "Active",
|
||||
"deleteTitle": "Delete skill",
|
||||
"deleteConfirm": "Are you sure you want to delete this skill?"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projects",
|
||||
"addNew": "Add project",
|
||||
"addNewTitle": "Add new project",
|
||||
"editTitle": "Edit project",
|
||||
"titlePl": "Title (PL)",
|
||||
"titleEn": "Title (EN)",
|
||||
"descriptionPl": "Description (PL)",
|
||||
"descriptionEn": "Description (EN)",
|
||||
"url": "Project URL",
|
||||
"repoUrl": "Repository URL (GitHub)",
|
||||
"repoUrl2": "Repository URL (Gitea)",
|
||||
"imageUrl": "Image URL",
|
||||
"order": "Order",
|
||||
"active": "Active",
|
||||
"deleteTitle": "Delete project",
|
||||
"deleteConfirm": "Are you sure you want to delete this project?"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"addNew": "Add contact item",
|
||||
"addNewTitle": "Add new contact item",
|
||||
"editTitle": "Edit contact item",
|
||||
"namePl": "Name (PL)",
|
||||
"nameEn": "Name (EN)",
|
||||
"iconName": "Icon name",
|
||||
"iconProvider": "Icon provider",
|
||||
"url": "URL",
|
||||
"external": "External link",
|
||||
"newTab": "Open in new tab",
|
||||
"order": "Order",
|
||||
"deleteTitle": "Delete contact item",
|
||||
"deleteConfirm": "Are you sure you want to delete this contact item?"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigation",
|
||||
"addNew": "Add navigation item",
|
||||
"addNewTitle": "Add new navigation item",
|
||||
"editTitle": "Edit navigation item",
|
||||
"labelPl": "Label (PL)",
|
||||
"labelEn": "Label (EN)",
|
||||
"url": "URL",
|
||||
"external": "External link",
|
||||
"newTab": "Open in new tab",
|
||||
"active": "Active",
|
||||
"order": "Order",
|
||||
"deleteTitle": "Delete navigation item",
|
||||
"deleteConfirm": "Are you sure you want to delete this navigation item?",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"topBar": {
|
||||
"title": "Top Bar",
|
||||
"addNew": "Add top bar item",
|
||||
"addNewTitle": "Add new top bar item",
|
||||
"editTitle": "Edit top bar item",
|
||||
"namePl": "Name (PL)",
|
||||
"nameEn": "Name (EN)",
|
||||
"iconName": "Icon name",
|
||||
"iconProvider": "Icon provider",
|
||||
"url": "URL",
|
||||
"external": "External link",
|
||||
"newTab": "Open in new tab",
|
||||
"order": "Order",
|
||||
"deleteTitle": "Delete top bar item",
|
||||
"deleteConfirm": "Are you sure you want to delete this top bar item?"
|
||||
},
|
||||
"metaTags": {
|
||||
"title": "Meta Tags",
|
||||
"description": "Manage SEO meta tags for the homepage",
|
||||
"projectsPageTitle": "Settings",
|
||||
"projectsPageDescription": "Manage SEO meta tags for the projects page",
|
||||
"skillsPageTitle": "Settings",
|
||||
"skillsPageDescription": "Manage SEO meta tags for the skills page",
|
||||
"contactPageTitle": "Settings",
|
||||
"contactPageDescription": "Manage SEO meta tags for the contact page",
|
||||
"metaTitle": "Meta Title",
|
||||
"metaTitlePlaceholder": "Enter page title for search engines...",
|
||||
"metaDescription": "Meta Description",
|
||||
"metaDescriptionPlaceholder": "Enter page description for search engines...",
|
||||
"metaKeywords": "Meta Keywords",
|
||||
"metaKeywordsPlaceholder": "Enter keywords separated by commas...",
|
||||
"ogImage": "Open Graph Image",
|
||||
"ogImagePlaceholder": "Enter image URL for social media sharing..."
|
||||
},
|
||||
"blog": {
|
||||
"title": "Blog",
|
||||
"posts": "Posts",
|
||||
"pageSettings": "Page Settings",
|
||||
"postsManagement": "Posts Management",
|
||||
"pageMetaTags": "Page Meta Tags",
|
||||
"newPost": "New Post",
|
||||
"noPosts": "No posts available",
|
||||
"searchPosts": "Search posts...",
|
||||
"deleteConfirm": "Are you sure you want to delete this post?",
|
||||
"published": "Published",
|
||||
"draft": "Draft",
|
||||
"neverPublished": "Never published",
|
||||
"category": "Category",
|
||||
"noCategory": "No category",
|
||||
"status": "Status",
|
||||
"publishedAt": "Published At",
|
||||
"views": "Views",
|
||||
"actions": "Actions",
|
||||
"view": "View",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"basicInfo": "Basic Information",
|
||||
"titlePlaceholder": "Enter post title...",
|
||||
"slug": "Slug",
|
||||
"slugPlaceholder": "my-blog-post-title",
|
||||
"content": "Content",
|
||||
"contentPlaceholder": "Write your post content here...",
|
||||
"excerpt": "Excerpt",
|
||||
"excerptPlaceholder": "Brief description of the post...",
|
||||
"publishSettings": "Publish Settings",
|
||||
"selectCategory": "Select a category",
|
||||
"publish": "Publish",
|
||||
"publishDate": "Publish Date",
|
||||
"featured": "Featured Post",
|
||||
"createPost": "Create Post",
|
||||
"postNotFound": "Post not found",
|
||||
"postInfo": "Post Information",
|
||||
"polishVersion": "Polish Version",
|
||||
"englishVersion": "English Version",
|
||||
"noContent": "No content available",
|
||||
"editPost": "Edit Post",
|
||||
"updatePost": "Update Post",
|
||||
"viewPost": "View Post",
|
||||
"categories": "Categories",
|
||||
"categoriesManagement": "Categories Management",
|
||||
"addCategory": "Add Category",
|
||||
"editCategory": "Edit Category",
|
||||
"deleteCategory": "Delete Category",
|
||||
"deleteCategoryConfirm": "Are you sure you want to delete this category? Posts in this category will remain but without category assignment.",
|
||||
"categoryNamePl": "Category Name (PL)",
|
||||
"categoryNameEn": "Category Name (EN)",
|
||||
"categorySlug": "Category Slug",
|
||||
"categoryDescriptionPl": "Description (PL)",
|
||||
"categoryDescriptionEn": "Description (EN)",
|
||||
"categoryActive": "Active",
|
||||
"iconName": "Icon Name",
|
||||
"iconProvider": "Icon Provider",
|
||||
"order": "Order",
|
||||
"noCategories": "No categories found",
|
||||
"subtitle": "Articles about programming, technology and software development",
|
||||
"searchPlaceholder": "Search articles...",
|
||||
"allCategories": "All",
|
||||
"featuredArticles": "Featured Articles",
|
||||
"allArticles": "All Articles",
|
||||
"noArticlesFound": "No articles found matching your search criteria.",
|
||||
"relatedArticles": "Related Articles",
|
||||
"backToBlog": "Back to blog",
|
||||
"articleNotFound": "Article not found",
|
||||
"readMore": "Read more",
|
||||
"rssFeed": "RSS Feed"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comments",
|
||||
"description": "Manage blog comments - approve, reject and delete comments",
|
||||
"searchPlaceholder": "Search by author, content or article title...",
|
||||
"statusUpdated": "Comment status has been updated",
|
||||
"updateError": "An error occurred while updating comment status",
|
||||
"deleted": "Comment has been deleted",
|
||||
"deleteError": "An error occurred while deleting comment",
|
||||
"confirmDelete": "Are you sure you want to delete this comment? This action cannot be undone.",
|
||||
"deleteTitle": "Delete Comment",
|
||||
"deleteMessage": "Are you sure you want to delete the comment from {authorName}? This action cannot be undone.",
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"total": "Total",
|
||||
"all": "All",
|
||||
"statusPending": "Pending",
|
||||
"statusApproved": "Approved",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"delete": "Delete",
|
||||
"article": "Article:",
|
||||
"replyToComment": "Reply to comment",
|
||||
"replyTo": "Reply to:",
|
||||
"noCommentsFound": "No comments found matching the search",
|
||||
"noComments": "No comments",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"replyButton": "Reply"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"checking": "Checking...",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"saved": "Changes saved successfully!",
|
||||
"add": "Add",
|
||||
"addImage": "Add image",
|
||||
"edit": "Edit",
|
||||
"update": "Update",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"actions": "Actions",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"updating": "Updating...",
|
||||
"updated": "Updated successfully",
|
||||
"deleting": "Deleting...",
|
||||
"deleted": "Deleted successfully",
|
||||
"select": "Select",
|
||||
"search": "Search",
|
||||
"changeOrder": "Change order mode",
|
||||
"back": "Back",
|
||||
"untitled": "Untitled",
|
||||
"draft": "Draft"
|
||||
},
|
||||
"fileUpload": {
|
||||
"uploading": "Uploading...",
|
||||
"dragAndDrop": "Drag & drop an image here, or",
|
||||
"chooseFile": "Choose File",
|
||||
"clear": "Clear",
|
||||
"selectOrUploadImage": "Select or upload an image"
|
||||
},
|
||||
"welcome": "Welcome",
|
||||
"login": {
|
||||
"title": "Admin Panel",
|
||||
"email": "Email address",
|
||||
"password": "Password",
|
||||
"signIn": "Sign in",
|
||||
"success": "Sign in successful",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "Don't have an account yet?",
|
||||
"register": "Register"
|
||||
},
|
||||
"register": {
|
||||
"title": "Register",
|
||||
"success": "Registration successful",
|
||||
"submit": "Register",
|
||||
"haveAccount": "Already have an account?",
|
||||
"signIn": "Sign in",
|
||||
"name": "Full name",
|
||||
"login": "Login"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"welcome": "Welcome back",
|
||||
"welcomeUser": "Welcome {name}",
|
||||
"privateData": "Private data: {{message}}",
|
||||
"nav": {
|
||||
"homepage": "Homepage",
|
||||
"skills": "Skills",
|
||||
"projects": "Projects",
|
||||
"navigation": "Navigation",
|
||||
"topbar": "Top Bar",
|
||||
"contact": "Contact",
|
||||
"blog": "Blog",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"newsletter": "Newsletter"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Return to website",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
"keyboardHint": "You can use arrow keys for navigation",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"theme": {
|
||||
"toggle": "Toggle theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"footer": {
|
||||
"copyright": "Bogusław Witek. All rights reserved."
|
||||
},
|
||||
"projects": {
|
||||
"title": "My Projects",
|
||||
"noProjects": "No projects to display",
|
||||
"github": "GitHub",
|
||||
"gitea": "Gitea",
|
||||
"demo": "Demo"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"noSkills": "No skills in this category",
|
||||
"noCategories": "No skill categories"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"directContact": "Contact Information",
|
||||
"quickMessage": "Quick Message",
|
||||
"form": {
|
||||
"name": "Full Name",
|
||||
"email": "Email Address",
|
||||
"message": "Message",
|
||||
"send": "Send Message",
|
||||
"success": "Message sent successfully!",
|
||||
"error": "An error occurred while sending the message. Please try again."
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"switch": "Switch language"
|
||||
},
|
||||
"userMenu": {
|
||||
"signIn": "Sign In",
|
||||
"signOut": "Sign Out",
|
||||
"myAccount": "My Account"
|
||||
},
|
||||
"home": {
|
||||
"apiStatus": "API Status"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"min": "This field must be at least {min} characters long",
|
||||
"max": "This field must be at most {max} characters long",
|
||||
"minLength": "This field must be at least {min} characters long",
|
||||
"maxLength": "This field must be at most {max} characters long",
|
||||
"minValue": "This field must be at least {min} value",
|
||||
"maxValue": "This field must be at most {max} value",
|
||||
"invalid": "This field is invalid",
|
||||
"invalidUrl": "Enter a valid URL (http:// or https://)",
|
||||
"turnstileRequired": "Please complete the security verification"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comments",
|
||||
"addComment": "Add Comment",
|
||||
"reply": "Reply",
|
||||
"replyButton": "Reply",
|
||||
"replyForm": "Reply to Comment",
|
||||
"noComments": "No comments yet. Be the first!",
|
||||
"moderationNotice": "Your comment will be published after approval by a moderator.",
|
||||
"loadError": "Cannot load comments - invalid article identifier.",
|
||||
"form": {
|
||||
"name": "Full Name",
|
||||
"namePlaceholder": "Enter your full name",
|
||||
"email": "Email Address",
|
||||
"emailPlaceholder": "Enter your email address (will not be public)",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://your-website.com (optional)",
|
||||
"content": "Comment Content",
|
||||
"contentPlaceholder": "Write your comment...",
|
||||
"submit": "Add Comment",
|
||||
"success": "Comment added and awaiting approval!",
|
||||
"error": "An error occurred while adding the comment. Please try again."
|
||||
}
|
||||
},
|
||||
"blog": {
|
||||
"title": "Blog",
|
||||
"subtitle": "Articles about programming, technology and software development",
|
||||
"searchPlaceholder": "Search articles...",
|
||||
"allCategories": "All",
|
||||
"featuredArticles": "Featured Articles",
|
||||
"allArticles": "All Articles",
|
||||
"noArticlesFound": "No articles found matching your search criteria.",
|
||||
"relatedArticles": "Related Articles",
|
||||
"backToBlog": "Back to blog",
|
||||
"articleNotFound": "Article not found",
|
||||
"readMore": "Read more",
|
||||
"featured": "Featured",
|
||||
"views": "views",
|
||||
"rssFeed": "RSS Feed",
|
||||
"share": {
|
||||
"title": "Share article",
|
||||
"facebook": "Share on Facebook",
|
||||
"twitter": "Share on Twitter",
|
||||
"linkedin": "Share on LinkedIn",
|
||||
"whatsapp": "Share via WhatsApp",
|
||||
"email": "Share via email",
|
||||
"copyLink": "Copy link",
|
||||
"linkCopied": "Link copied to clipboard!"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page not found",
|
||||
"description": "The page you are looking for was not found. It may have been moved or deleted.",
|
||||
"homepage": "Homepage"
|
||||
},
|
||||
"newsletter": {
|
||||
"title": "Newsletter",
|
||||
"description": "Get notified about new articles",
|
||||
"email": "Email address",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"language": "Newsletter language",
|
||||
"gdprConsent": "I consent to data processing for newsletter purposes in accordance with the",
|
||||
"gdprConsentCompact": "I consent to the processing of my personal data for receiving the newsletter.",
|
||||
"gdprConsentError": "You must consent to data processing",
|
||||
"privacyPolicy": "privacy policy",
|
||||
"subscribe": "Subscribe to newsletter",
|
||||
"subscribeShort": "Subscribe",
|
||||
"subscribing": "Subscribing...",
|
||||
"successMessage": "Successfully subscribed to newsletter!",
|
||||
"errorMessage": "An error occurred during subscription",
|
||||
"blogSectionTitle": "Subscribe to Newsletter",
|
||||
"blogSectionDescription": "Get notifications about new articles directly to your email inbox",
|
||||
"articleSectionTitle": "Did you enjoy this article?",
|
||||
"articleSectionDescription": "Subscribe to our newsletter to not miss the next ones!",
|
||||
"contentPlaceholders": {
|
||||
"privacyPolicyPl": "Enter privacy policy content in Polish...",
|
||||
"privacyPolicyEn": "Enter privacy policy content in English..."
|
||||
},
|
||||
"languages": {
|
||||
"polish": "Polish",
|
||||
"english": "English"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Newsletter Management",
|
||||
"description": "Manage newsletter subscriptions and send notifications to subscribers",
|
||||
"stats": {
|
||||
"title": "Newsletter Statistics",
|
||||
"description": "Current subscriber counts by language",
|
||||
"polish": "Polish Subscribers",
|
||||
"english": "English Subscribers",
|
||||
"total": "Total Subscribers",
|
||||
"loading": "Loading statistics..."
|
||||
},
|
||||
"send": {
|
||||
"title": "Send Newsletter",
|
||||
"description": "Send a newsletter to subscribers about a published article",
|
||||
"selectArticle": "Select Article",
|
||||
"articlePlaceholder": "Choose an article to feature...",
|
||||
"preview": "Selected Article Preview:",
|
||||
"polishTitle": "Polish Title:",
|
||||
"englishTitle": "English Title:",
|
||||
"slug": "Slug:",
|
||||
"status": "Status:",
|
||||
"published": "Published",
|
||||
"draft": "Draft",
|
||||
"notAvailable": "Not available",
|
||||
"sendToPolish": "Send to Polish Subscribers",
|
||||
"sendToEnglish": "Send to English Subscribers",
|
||||
"confirmTitle": "Confirm Newsletter Send",
|
||||
"confirmDescription": "This action cannot be undone. The newsletter will be sent immediately to all subscribers.",
|
||||
"confirmDetails": {
|
||||
"article": "Article:",
|
||||
"language": "Language:",
|
||||
"recipients": "Recipients:",
|
||||
"subscribers": "subscribers"
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"sending": "Sending...",
|
||||
"confirm": "Yes, Send Newsletter",
|
||||
"selectArticleFirst": "Please select an article first",
|
||||
"successMessage": "Newsletter sent to {count} {language} subscribers",
|
||||
"polish": "Polish",
|
||||
"english": "English",
|
||||
"unsubscribe": {
|
||||
"title": "Unsubscribe from newsletter",
|
||||
"subtitle": "We're sorry to see you go. Your feedback will help us improve.",
|
||||
"emailAddress": "Email address",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"reason": "Reason for unsubscribing (optional)",
|
||||
"reasons": {
|
||||
"too_frequent": "Too frequent emails",
|
||||
"not_relevant": "Content is not relevant to me",
|
||||
"never_subscribed": "I never subscribed",
|
||||
"poor_content": "Poor content quality",
|
||||
"technical_issues": "Technical issues",
|
||||
"other": "Other reason"
|
||||
},
|
||||
"feedback": "Additional feedback",
|
||||
"feedbackPlaceholder": "Tell us more about your feedback...",
|
||||
"cancel": "Cancel",
|
||||
"unsubscribeAction": "Unsubscribe",
|
||||
"unsubscribing": "Unsubscribing...",
|
||||
"emailRequired": "Email address is required",
|
||||
"successTitle": "Unsubscribed successfully",
|
||||
"successMessage": "You have been successfully unsubscribed from BWitek.dev newsletter. Thank you for your feedback!",
|
||||
"feedbackNote": "Your feedback helps us better tailor our content to our readers' needs.",
|
||||
"home": "Home",
|
||||
"blog": "Blog"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"richEditor": {
|
||||
"toolbar": {
|
||||
"bold": "Bold",
|
||||
"italic": "Italic",
|
||||
"strikethrough": "Strikethrough",
|
||||
"code": "Inline Code",
|
||||
"heading1": "Heading 1",
|
||||
"heading2": "Heading 2",
|
||||
"heading3": "Heading 3",
|
||||
"bulletList": "Bullet List",
|
||||
"orderedList": "Ordered List",
|
||||
"quote": "Quote",
|
||||
"addLink": "Add Link",
|
||||
"addImage": "Add Image",
|
||||
"addYoutube": "Add YouTube Video",
|
||||
"addCodeBlock": "Add Code Block",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
},
|
||||
"modals": {
|
||||
"link": {
|
||||
"title": "Add Link",
|
||||
"description": "Enter the URL of the page you want to link to. Leave empty to remove link.",
|
||||
"urlLabel": "URL",
|
||||
"urlPlaceholder": "https://example.com",
|
||||
"cancel": "Cancel",
|
||||
"addLink": "Add Link",
|
||||
"removeLink": "Remove Link"
|
||||
},
|
||||
"youtube": {
|
||||
"title": "Add YouTube Video",
|
||||
"description": "Enter a YouTube video link in one of the supported formats.",
|
||||
"urlLabel": "YouTube URL",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"supportedFormats": "Supported formats:",
|
||||
"formats": [
|
||||
"https://www.youtube.com/watch?v=VIDEO_ID",
|
||||
"https://youtu.be/VIDEO_ID",
|
||||
"https://www.youtube.com/embed/VIDEO_ID"
|
||||
],
|
||||
"invalidUrl": "Invalid YouTube URL format",
|
||||
"cancel": "Cancel",
|
||||
"addVideo": "Add Video"
|
||||
},
|
||||
"codeBlock": {
|
||||
"title": "Add Code Block",
|
||||
"description": "Enter a programming language for syntax highlighting (optional).",
|
||||
"languageLabel": "Programming Language",
|
||||
"languagePlaceholder": "javascript, python, css...",
|
||||
"popularLanguages": "Popular languages:",
|
||||
"languagesList": "javascript, typescript, python, css, html, json, sql, bash, php, java, cpp",
|
||||
"noHighlighting": "Leave empty for plain text without highlighting.",
|
||||
"cancel": "Cancel",
|
||||
"addCodeBlock": "Add Code Block"
|
||||
},
|
||||
"image": {
|
||||
"title": "Add Image",
|
||||
"description": "Select or upload a new image for the article.",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
235
apps/web/messages/en/admin.json
Normal file
235
apps/web/messages/en/admin.json
Normal file
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "Admin Panel",
|
||||
"description": "Welcome to the admin panel. Choose a section you want to edit."
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Homepage",
|
||||
"description": "Edit homepage content",
|
||||
"welcomeText": "Welcome Text",
|
||||
"welcomePlaceholder": "Enter welcome text...",
|
||||
"specializationText": "Specialization",
|
||||
"specializationPlaceholder": "Enter specialization text...",
|
||||
"aboutMeText": "About Me",
|
||||
"aboutMePlaceholder": "Enter about me text...",
|
||||
"updateSuccess": "Homepage updated successfully"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"addNew": "Add skill",
|
||||
"addNewTitle": "Add new skill",
|
||||
"editTitle": "Edit skill",
|
||||
"namePl": "Name (PL)",
|
||||
"nameEn": "Name (EN)",
|
||||
"categoryPl": "Category (PL)",
|
||||
"categoryEn": "Category (EN)",
|
||||
"category": "Category",
|
||||
"selectCategory": "Select category",
|
||||
"categoriesTitle": "Skill categories",
|
||||
"addNewCategory": "Add category",
|
||||
"addNewCategoryTitle": "Add new category",
|
||||
"editCategoryTitle": "Edit category",
|
||||
"deleteCategoryTitle": "Delete category",
|
||||
"deleteCategoryConfirm": "Are you sure you want to delete this category? Skills assigned to this category will not be assigned to any category.",
|
||||
"icon": "Icon",
|
||||
"iconName": "Icon name",
|
||||
"iconProvider": "Icon provider",
|
||||
"order": "Order",
|
||||
"active": "Active",
|
||||
"deleteTitle": "Delete skill",
|
||||
"deleteConfirm": "Are you sure you want to delete this skill?"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projects",
|
||||
"addNew": "Add project",
|
||||
"addNewTitle": "Add new project",
|
||||
"editTitle": "Edit project",
|
||||
"titlePl": "Title (PL)",
|
||||
"titleEn": "Title (EN)",
|
||||
"descriptionPl": "Description (PL)",
|
||||
"descriptionEn": "Description (EN)",
|
||||
"url": "Project URL",
|
||||
"repoUrl": "Repository URL (GitHub)",
|
||||
"repoUrl2": "Repository URL (Gitea)",
|
||||
"imageUrl": "Image URL",
|
||||
"order": "Order",
|
||||
"active": "Active",
|
||||
"deleteTitle": "Delete project",
|
||||
"deleteConfirm": "Are you sure you want to delete this project?"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"addNew": "Add contact item",
|
||||
"addNewTitle": "Add new contact item",
|
||||
"editTitle": "Edit contact item",
|
||||
"namePl": "Name (PL)",
|
||||
"nameEn": "Name (EN)",
|
||||
"iconName": "Icon name",
|
||||
"iconProvider": "Icon provider",
|
||||
"url": "URL",
|
||||
"external": "External link",
|
||||
"newTab": "Open in new tab",
|
||||
"order": "Order",
|
||||
"deleteTitle": "Delete contact item",
|
||||
"deleteConfirm": "Are you sure you want to delete this contact item?"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigation",
|
||||
"addNew": "Add navigation item",
|
||||
"addNewTitle": "Add new navigation item",
|
||||
"editTitle": "Edit navigation item",
|
||||
"labelPl": "Label (PL)",
|
||||
"labelEn": "Label (EN)",
|
||||
"url": "URL",
|
||||
"external": "External link",
|
||||
"newTab": "Open in new tab",
|
||||
"active": "Active",
|
||||
"order": "Order",
|
||||
"deleteTitle": "Delete navigation item",
|
||||
"deleteConfirm": "Are you sure you want to delete this navigation item?",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"topBar": {
|
||||
"title": "Top Bar",
|
||||
"addNew": "Add top bar item",
|
||||
"addNewTitle": "Add new top bar item",
|
||||
"editTitle": "Edit top bar item",
|
||||
"namePl": "Name (PL)",
|
||||
"nameEn": "Name (EN)",
|
||||
"iconName": "Icon name",
|
||||
"iconProvider": "Icon provider",
|
||||
"url": "URL",
|
||||
"external": "External link",
|
||||
"newTab": "Open in new tab",
|
||||
"order": "Order",
|
||||
"deleteTitle": "Delete top bar item",
|
||||
"deleteConfirm": "Are you sure you want to delete this top bar item?"
|
||||
},
|
||||
"metaTags": {
|
||||
"title": "Meta Tags",
|
||||
"description": "Manage SEO meta tags for the homepage",
|
||||
"projectsPageTitle": "Settings",
|
||||
"projectsPageDescription": "Manage SEO meta tags for the projects page",
|
||||
"skillsPageTitle": "Settings",
|
||||
"skillsPageDescription": "Manage SEO meta tags for the skills page",
|
||||
"contactPageTitle": "Settings",
|
||||
"contactPageDescription": "Manage SEO meta tags for the contact page",
|
||||
"metaTitle": "Meta Title",
|
||||
"metaTitlePlaceholder": "Enter page title for search engines...",
|
||||
"metaDescription": "Meta Description",
|
||||
"metaDescriptionPlaceholder": "Enter page description for search engines...",
|
||||
"metaKeywords": "Meta Keywords",
|
||||
"metaKeywordsPlaceholder": "Enter keywords separated by commas...",
|
||||
"ogImage": "Open Graph Image",
|
||||
"ogImagePlaceholder": "Enter image URL for social media sharing..."
|
||||
},
|
||||
"blog": {
|
||||
"title": "Blog",
|
||||
"posts": "Posts",
|
||||
"pageSettings": "Page Settings",
|
||||
"postsManagement": "Posts Management",
|
||||
"pageMetaTags": "Page Meta Tags",
|
||||
"newPost": "New Post",
|
||||
"noPosts": "No posts available",
|
||||
"searchPosts": "Search posts...",
|
||||
"deleteConfirm": "Are you sure you want to delete this post?",
|
||||
"published": "Published",
|
||||
"draft": "Draft",
|
||||
"neverPublished": "Never published",
|
||||
"category": "Category",
|
||||
"noCategory": "No category",
|
||||
"status": "Status",
|
||||
"publishedAt": "Published At",
|
||||
"views": "Views",
|
||||
"actions": "Actions",
|
||||
"view": "View",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"basicInfo": "Basic Information",
|
||||
"titlePlaceholder": "Enter post title...",
|
||||
"slug": "Slug",
|
||||
"slugPlaceholder": "my-blog-post-title",
|
||||
"content": "Content",
|
||||
"contentPlaceholder": "Write your post content here...",
|
||||
"excerpt": "Excerpt",
|
||||
"excerptPlaceholder": "Brief description of the post...",
|
||||
"publishSettings": "Publish Settings",
|
||||
"selectCategory": "Select a category",
|
||||
"publish": "Publish",
|
||||
"publishDate": "Publish Date",
|
||||
"featured": "Featured Post",
|
||||
"createPost": "Create Post",
|
||||
"postNotFound": "Post not found",
|
||||
"postInfo": "Post Information",
|
||||
"polishVersion": "Polish Version",
|
||||
"englishVersion": "English Version",
|
||||
"noContent": "No content available",
|
||||
"editPost": "Edit Post",
|
||||
"updatePost": "Update Post",
|
||||
"viewPost": "View Post",
|
||||
"categories": "Categories",
|
||||
"categoriesManagement": "Categories Management",
|
||||
"addCategory": "Add Category",
|
||||
"editCategory": "Edit Category",
|
||||
"deleteCategory": "Delete Category",
|
||||
"deleteCategoryConfirm": "Are you sure you want to delete this category? Posts in this category will remain but without category assignment.",
|
||||
"categoryNamePl": "Category Name (PL)",
|
||||
"categoryNameEn": "Category Name (EN)",
|
||||
"categorySlug": "Category Slug",
|
||||
"categoryDescriptionPl": "Description (PL)",
|
||||
"categoryDescriptionEn": "Description (EN)",
|
||||
"categoryActive": "Active",
|
||||
"iconName": "Icon Name",
|
||||
"iconProvider": "Icon Provider",
|
||||
"order": "Order",
|
||||
"noCategories": "No categories found",
|
||||
"subtitle": "Articles about programming, technology and software development",
|
||||
"searchPlaceholder": "Search articles...",
|
||||
"allCategories": "All",
|
||||
"featuredArticles": "Featured Articles",
|
||||
"allArticles": "All Articles",
|
||||
"noArticlesFound": "No articles found matching your search criteria.",
|
||||
"relatedArticles": "Related Articles",
|
||||
"backToBlog": "Back to blog",
|
||||
"articleNotFound": "Article not found",
|
||||
"readMore": "Read more",
|
||||
"rssFeed": "RSS Feed"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comments",
|
||||
"description": "Manage blog comments - approve, reject and delete comments",
|
||||
"searchPlaceholder": "Search by author, content or article title...",
|
||||
"statusUpdated": "Comment status has been updated",
|
||||
"updateError": "An error occurred while updating comment status",
|
||||
"deleted": "Comment has been deleted",
|
||||
"deleteError": "An error occurred while deleting comment",
|
||||
"confirmDelete": "Are you sure you want to delete this comment? This action cannot be undone.",
|
||||
"deleteTitle": "Delete Comment",
|
||||
"deleteMessage": "Are you sure you want to delete the comment from {authorName}? This action cannot be undone.",
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"total": "Total",
|
||||
"all": "All",
|
||||
"statusPending": "Pending",
|
||||
"statusApproved": "Approved",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"delete": "Delete",
|
||||
"article": "Article:",
|
||||
"replyToComment": "Reply to comment",
|
||||
"replyTo": "Reply to:",
|
||||
"noCommentsFound": "No comments found matching the search",
|
||||
"noComments": "No comments",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"replyButton": "Reply"
|
||||
},
|
||||
"privacy-policy": {
|
||||
"title": "Privacy Policy",
|
||||
"metaTags": "Settings",
|
||||
"metaTagsDescription": "Manage SEO meta tags for the privacy policy page",
|
||||
"plContent": "Polish content",
|
||||
"enContent": "English content",
|
||||
"content": "Privacy Policy Content"
|
||||
}
|
||||
}
|
||||
26
apps/web/messages/en/blog.json
Normal file
26
apps/web/messages/en/blog.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"title": "Blog",
|
||||
"subtitle": "Articles about programming, technology and software development",
|
||||
"searchPlaceholder": "Search articles...",
|
||||
"allCategories": "All",
|
||||
"featuredArticles": "Featured Articles",
|
||||
"allArticles": "All Articles",
|
||||
"noArticlesFound": "No articles found matching your search criteria.",
|
||||
"relatedArticles": "Related Articles",
|
||||
"backToBlog": "Back to blog",
|
||||
"articleNotFound": "Article not found",
|
||||
"readMore": "Read more",
|
||||
"featured": "Featured",
|
||||
"views": "views",
|
||||
"rssFeed": "RSS Feed",
|
||||
"share": {
|
||||
"title": "Share article",
|
||||
"facebook": "Share on Facebook",
|
||||
"twitter": "Share on Twitter",
|
||||
"linkedin": "Share on LinkedIn",
|
||||
"whatsapp": "Share via WhatsApp",
|
||||
"email": "Share via email",
|
||||
"copyLink": "Copy link",
|
||||
"linkCopied": "Link copied to clipboard!"
|
||||
}
|
||||
}
|
||||
23
apps/web/messages/en/comments.json
Normal file
23
apps/web/messages/en/comments.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"title": "Comments",
|
||||
"addComment": "Add Comment",
|
||||
"reply": "Reply",
|
||||
"replyButton": "Reply",
|
||||
"replyForm": "Reply to Comment",
|
||||
"noComments": "No comments yet. Be the first!",
|
||||
"moderationNotice": "Your comment will be published after approval by a moderator.",
|
||||
"loadError": "Cannot load comments - invalid article identifier.",
|
||||
"form": {
|
||||
"name": "Full Name",
|
||||
"namePlaceholder": "Enter your full name",
|
||||
"email": "Email Address",
|
||||
"emailPlaceholder": "Enter your email address (will not be public)",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://your-website.com (optional)",
|
||||
"content": "Comment Content",
|
||||
"contentPlaceholder": "Write your comment...",
|
||||
"submit": "Add Comment",
|
||||
"success": "Comment added and awaiting approval!",
|
||||
"error": "An error occurred while adding the comment. Please try again."
|
||||
}
|
||||
}
|
||||
33
apps/web/messages/en/common.json
Normal file
33
apps/web/messages/en/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"welcome": "Welcome",
|
||||
"loading": "Loading...",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"checking": "Checking...",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"saved": "Changes saved successfully!",
|
||||
"submitting": "Submitting...",
|
||||
"add": "Add",
|
||||
"addImage": "Add image",
|
||||
"edit": "Edit",
|
||||
"update": "Update",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"actions": "Actions",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"updating": "Updating...",
|
||||
"updated": "Updated successfully",
|
||||
"deleting": "Deleting...",
|
||||
"deleted": "Deleted successfully",
|
||||
"select": "Select",
|
||||
"search": "Search",
|
||||
"changeOrder": "Change order mode",
|
||||
"back": "Back",
|
||||
"untitled": "Untitled",
|
||||
"draft": "Draft",
|
||||
"showMore": "Show more",
|
||||
"noResults": "No results found",
|
||||
"noData": "No data"
|
||||
}
|
||||
107
apps/web/messages/en/components.json
Normal file
107
apps/web/messages/en/components.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"fileUpload": {
|
||||
"uploading": "Uploading...",
|
||||
"dragAndDrop": "Drag & drop an image here, or",
|
||||
"chooseFile": "Choose File",
|
||||
"clear": "Clear",
|
||||
"selectOrUploadImage": "Select or upload an image"
|
||||
},
|
||||
"richEditor": {
|
||||
"toolbar": {
|
||||
"bold": "Bold",
|
||||
"italic": "Italic",
|
||||
"strikethrough": "Strikethrough",
|
||||
"code": "Inline Code",
|
||||
"heading1": "Heading 1",
|
||||
"heading2": "Heading 2",
|
||||
"heading3": "Heading 3",
|
||||
"bulletList": "Bullet List",
|
||||
"orderedList": "Ordered List",
|
||||
"quote": "Quote",
|
||||
"addLink": "Add Link",
|
||||
"addImage": "Add Image",
|
||||
"addYoutube": "Add YouTube Video",
|
||||
"addCodeBlock": "Add Code Block",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
},
|
||||
"modals": {
|
||||
"link": {
|
||||
"title": "Add Link",
|
||||
"description": "Enter the URL of the page you want to link to. Leave empty to remove link.",
|
||||
"urlLabel": "URL",
|
||||
"urlPlaceholder": "https://example.com",
|
||||
"cancel": "Cancel",
|
||||
"addLink": "Add Link",
|
||||
"removeLink": "Remove Link"
|
||||
},
|
||||
"youtube": {
|
||||
"title": "Add YouTube Video",
|
||||
"description": "Enter a YouTube video link in one of the supported formats.",
|
||||
"urlLabel": "YouTube URL",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"supportedFormats": "Supported formats:",
|
||||
"formats": [
|
||||
"https://www.youtube.com/watch?v=VIDEO_ID",
|
||||
"https://youtu.be/VIDEO_ID",
|
||||
"https://www.youtube.com/embed/VIDEO_ID"
|
||||
],
|
||||
"invalidUrl": "Invalid YouTube URL format",
|
||||
"cancel": "Cancel",
|
||||
"addVideo": "Add Video"
|
||||
},
|
||||
"codeBlock": {
|
||||
"title": "Add Code Block",
|
||||
"description": "Enter a programming language for syntax highlighting (optional).",
|
||||
"languageLabel": "Programming Language",
|
||||
"languagePlaceholder": "javascript, python, css...",
|
||||
"popularLanguages": "Popular languages:",
|
||||
"languagesList": "javascript, typescript, python, css, html, json, sql, bash, php, java, cpp",
|
||||
"noHighlighting": "Leave empty for plain text without highlighting.",
|
||||
"cancel": "Cancel",
|
||||
"addCodeBlock": "Add Code Block"
|
||||
},
|
||||
"image": {
|
||||
"title": "Add Image",
|
||||
"description": "Select or upload a new image for the article.",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"switch": "Switch language"
|
||||
},
|
||||
"userMenu": {
|
||||
"signIn": "Sign In",
|
||||
"signOut": "Sign Out",
|
||||
"myAccount": "My Account"
|
||||
},
|
||||
"theme": {
|
||||
"toggle": "Toggle theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Return to website",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
"keyboardHint": "You can use arrow keys for navigation",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"footer": {
|
||||
"copyright": "Bogusław Witek. All rights reserved."
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"min": "This field must be at least {min} characters long",
|
||||
"max": "This field must be at most {max} characters long",
|
||||
"minLength": "This field must be at least {min} characters long",
|
||||
"maxLength": "This field must be at most {max} characters long",
|
||||
"minValue": "This field must be at least {min} value",
|
||||
"maxValue": "This field must be at most {max} value",
|
||||
"invalid": "This field is invalid",
|
||||
"invalidUrl": "Enter a valid URL (http:// or https://)",
|
||||
"turnstileRequired": "Please complete the security verification"
|
||||
}
|
||||
}
|
||||
97
apps/web/messages/en/newsletter.json
Normal file
97
apps/web/messages/en/newsletter.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"title": "Newsletter",
|
||||
"description": "Get notified about new articles",
|
||||
"email": "Email address",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"language": "Newsletter language",
|
||||
"gdprConsent": "I consent to data processing for newsletter purposes in accordance with the",
|
||||
"gdprConsentCompact": "I consent to the processing of my personal data for receiving the newsletter.",
|
||||
"gdprConsentError": "You must consent to data processing",
|
||||
"privacyPolicy": "privacy policy",
|
||||
"subscribe": "Subscribe to newsletter",
|
||||
"subscribeShort": "Subscribe",
|
||||
"subscribing": "Subscribing...",
|
||||
"successMessage": "Successfully subscribed to newsletter!",
|
||||
"errorMessage": "An error occurred during subscription",
|
||||
"blogSectionTitle": "Subscribe to Newsletter",
|
||||
"blogSectionDescription": "Get notifications about new articles directly to your email inbox",
|
||||
"articleSectionTitle": "Did you enjoy this article?",
|
||||
"articleSectionDescription": "Subscribe to our newsletter to not miss the next ones!",
|
||||
"contentPlaceholders": {
|
||||
"privacyPolicyPl": "Enter privacy policy content in Polish...",
|
||||
"privacyPolicyEn": "Enter privacy policy content in English..."
|
||||
},
|
||||
"languages": {
|
||||
"polish": "Polish",
|
||||
"english": "English"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Newsletter Management",
|
||||
"description": "Manage newsletter subscriptions and send notifications to subscribers",
|
||||
"stats": {
|
||||
"title": "Newsletter Statistics",
|
||||
"description": "Current subscriber counts by language",
|
||||
"polish": "Polish Subscribers",
|
||||
"english": "English Subscribers",
|
||||
"total": "Total Subscribers",
|
||||
"loading": "Loading statistics..."
|
||||
},
|
||||
"send": {
|
||||
"title": "Send Newsletter",
|
||||
"description": "Send a newsletter to subscribers about a published article",
|
||||
"selectArticle": "Select Article",
|
||||
"articlePlaceholder": "Choose an article to feature...",
|
||||
"preview": "Selected Article Preview:",
|
||||
"polishTitle": "Polish Title:",
|
||||
"englishTitle": "English Title:",
|
||||
"slug": "Slug:",
|
||||
"status": "Status:",
|
||||
"published": "Published",
|
||||
"draft": "Draft",
|
||||
"notAvailable": "Not available",
|
||||
"sendToPolish": "Send to Polish Subscribers",
|
||||
"sendToEnglish": "Send to English Subscribers",
|
||||
"confirmTitle": "Confirm Newsletter Send",
|
||||
"confirmDescription": "This action cannot be undone. The newsletter will be sent immediately to all subscribers.",
|
||||
"confirmDetails": {
|
||||
"article": "Article:",
|
||||
"language": "Language:",
|
||||
"recipients": "Recipients:",
|
||||
"subscribers": "subscribers"
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"sending": "Sending...",
|
||||
"confirm": "Yes, Send Newsletter",
|
||||
"selectArticleFirst": "Please select an article first",
|
||||
"successMessage": "Newsletter sent to {count} {language} subscribers",
|
||||
"polish": "Polish",
|
||||
"english": "English",
|
||||
"unsubscribe": {
|
||||
"title": "Unsubscribe from newsletter",
|
||||
"subtitle": "We're sorry to see you go. Your feedback will help us improve.",
|
||||
"emailAddress": "Email address",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"reason": "Reason for unsubscribing (optional)",
|
||||
"reasons": {
|
||||
"too_frequent": "Too frequent emails",
|
||||
"not_relevant": "Content is not relevant to me",
|
||||
"never_subscribed": "I never subscribed",
|
||||
"poor_content": "Poor content quality",
|
||||
"technical_issues": "Technical issues",
|
||||
"other": "Other reason"
|
||||
},
|
||||
"feedback": "Additional feedback",
|
||||
"feedbackPlaceholder": "Tell us more about your feedback...",
|
||||
"cancel": "Cancel",
|
||||
"unsubscribeAction": "Unsubscribe",
|
||||
"unsubscribing": "Unsubscribing...",
|
||||
"emailRequired": "Email address is required",
|
||||
"successTitle": "Unsubscribed successfully",
|
||||
"successMessage": "You have been successfully unsubscribed from BWitek.dev newsletter. Thank you for your feedback!",
|
||||
"feedbackNote": "Your feedback helps us better tailor our content to our readers' needs.",
|
||||
"home": "Home",
|
||||
"blog": "Blog"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
apps/web/messages/en/pages.json
Normal file
68
apps/web/messages/en/pages.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Admin Panel",
|
||||
"email": "Email address",
|
||||
"password": "Password",
|
||||
"signIn": "Sign in",
|
||||
"success": "Sign in successful",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "Don't have an account yet?",
|
||||
"register": "Register"
|
||||
},
|
||||
"register": {
|
||||
"title": "Register",
|
||||
"success": "Registration successful",
|
||||
"submit": "Register",
|
||||
"haveAccount": "Already have an account?",
|
||||
"signIn": "Sign in",
|
||||
"name": "Full name",
|
||||
"login": "Login"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"welcome": "Welcome back",
|
||||
"welcomeUser": "Welcome {name}",
|
||||
"privateData": "Private data: {{message}}",
|
||||
"nav": {
|
||||
"homepage": "Homepage",
|
||||
"skills": "Skills",
|
||||
"projects": "Projects",
|
||||
"navigation": "Navigation",
|
||||
"topbar": "Top Bar",
|
||||
"contact": "Contact",
|
||||
"blog": "Blog",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"newsletter": "Newsletter"
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"title": "My Projects",
|
||||
"noProjects": "No projects to display",
|
||||
"github": "GitHub",
|
||||
"gitea": "Gitea",
|
||||
"demo": "Demo"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"noSkills": "No skills in this category",
|
||||
"noCategories": "No skill categories"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"directContact": "Contact Information",
|
||||
"quickMessage": "Quick Message",
|
||||
"form": {
|
||||
"name": "Full Name",
|
||||
"email": "Email Address",
|
||||
"message": "Message",
|
||||
"send": "Send Message",
|
||||
"success": "Message sent successfully!",
|
||||
"error": "An error occurred while sending the message. Please try again."
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page not found",
|
||||
"description": "The page you are looking for was not found. It may have been moved or deleted.",
|
||||
"homepage": "Homepage"
|
||||
}
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
{
|
||||
"admin": {
|
||||
"dashboard": {
|
||||
"title": "Panel Administracyjny",
|
||||
"description": "Witaj w panelu administracyjnym. Wybierz sekcję, którą chcesz edytować."
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Strona Główna",
|
||||
"description": "Edytuj zawartość strony głównej",
|
||||
"welcomeText": "Tekst powitalny",
|
||||
"welcomePlaceholder": "Wprowadź tekst powitalny...",
|
||||
"specializationText": "Specjalizacja",
|
||||
"specializationPlaceholder": "Wprowadź tekst o specjalizacji...",
|
||||
"aboutMeText": "O mnie",
|
||||
"aboutMePlaceholder": "Wprowadź tekst o sobie...",
|
||||
"updateSuccess": "Pomyślnie zaktualizowano stronę główną"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Umiejętności",
|
||||
"addNew": "Dodaj umiejętność",
|
||||
"addNewTitle": "Dodaj nową umiejętność",
|
||||
"editTitle": "Edytuj umiejętność",
|
||||
"namePl": "Nazwa (PL)",
|
||||
"nameEn": "Nazwa (EN)",
|
||||
"categoryPl": "Kategoria (PL)",
|
||||
"categoryEn": "Kategoria (EN)",
|
||||
"category": "Kategoria",
|
||||
"selectCategory": "Wybierz kategorię",
|
||||
"categoriesTitle": "Kategorie umiejętności",
|
||||
"addNewCategory": "Dodaj kategorię",
|
||||
"addNewCategoryTitle": "Dodaj nową kategorię",
|
||||
"editCategoryTitle": "Edytuj kategorię",
|
||||
"deleteCategoryTitle": "Usuwanie kategorii",
|
||||
"deleteCategoryConfirm": "Czy na pewno chcesz usunąć tę kategorię? Umiejętności przypisane do tej kategorii nie będą przypisane do żadnej kategorii.",
|
||||
"icon": "Ikona",
|
||||
"iconName": "Nazwa ikony",
|
||||
"iconProvider": "Dostawca ikony",
|
||||
"order": "Kolejność",
|
||||
"active": "Aktywny",
|
||||
"deleteTitle": "Usuwanie umiejętności",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć tę umiejętność?"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projekty",
|
||||
"addNew": "Dodaj projekt",
|
||||
"addNewTitle": "Dodaj nowy projekt",
|
||||
"editTitle": "Edytuj projekt",
|
||||
"titlePl": "Tytuł (PL)",
|
||||
"titleEn": "Tytuł (EN)",
|
||||
"descriptionPl": "Opis (PL)",
|
||||
"descriptionEn": "Opis (EN)",
|
||||
"url": "Adres URL projektu",
|
||||
"repoUrl": "Adres URL repozytorium (GitHub)",
|
||||
"repoUrl2": "Adres URL repozytorium (Gitea)",
|
||||
"imageUrl": "Adres URL obrazka",
|
||||
"order": "Kolejność",
|
||||
"active": "Aktywny",
|
||||
"deleteTitle": "Usuwanie projektu",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten projekt?"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Kontakt",
|
||||
"addNew": "Dodaj element kontaktu",
|
||||
"addNewTitle": "Dodaj nowy element kontaktu",
|
||||
"editTitle": "Edytuj element kontaktu",
|
||||
"namePl": "Nazwa (PL)",
|
||||
"nameEn": "Nazwa (EN)",
|
||||
"iconName": "Nazwa ikony",
|
||||
"iconProvider": "Dostawca ikony",
|
||||
"url": "URL",
|
||||
"external": "Link zewnętrzny",
|
||||
"newTab": "Otwórz w nowej karcie",
|
||||
"order": "Kolejność",
|
||||
"deleteTitle": "Usuwanie elementu kontaktu",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten element kontaktu?"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Nawigacja",
|
||||
"addNew": "Dodaj element nawigacji",
|
||||
"addNewTitle": "Dodaj nowy element nawigacji",
|
||||
"editTitle": "Edytuj element nawigacji",
|
||||
"labelPl": "Etykieta (PL)",
|
||||
"labelEn": "Etykieta (EN)",
|
||||
"url": "URL",
|
||||
"external": "Link zewnętrzny",
|
||||
"newTab": "Otwórz w nowej karcie",
|
||||
"active": "Aktywny",
|
||||
"order": "Kolejność",
|
||||
"deleteTitle": "Usuwanie elementu nawigacji",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten element nawigacji?"
|
||||
},
|
||||
"topBar": {
|
||||
"title": "Górny pasek",
|
||||
"addNew": "Dodaj element górnego paska",
|
||||
"addNewTitle": "Dodaj nowy element górnego paska",
|
||||
"editTitle": "Edytuj element górnego paska",
|
||||
"namePl": "Nazwa (PL)",
|
||||
"nameEn": "Nazwa (EN)",
|
||||
"iconName": "Nazwa ikony",
|
||||
"iconProvider": "Dostawca ikony",
|
||||
"url": "URL",
|
||||
"external": "Link zewnętrzny",
|
||||
"newTab": "Otwórz w nowej karcie",
|
||||
"order": "Kolejność",
|
||||
"deleteTitle": "Usuwanie elementu górnego paska",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten element górnego paska?"
|
||||
},
|
||||
"metaTags": {
|
||||
"title": "Meta Tagi",
|
||||
"description": "Zarządzaj meta tagami SEO dla strony głównej",
|
||||
"projectsPageTitle": "Ustawienia",
|
||||
"projectsPageDescription": "Zarządzaj meta tagami SEO dla strony projektów",
|
||||
"skillsPageTitle": "Ustawienia",
|
||||
"skillsPageDescription": "Zarządzaj meta tagami SEO dla strony umiejętności",
|
||||
"contactPageTitle": "Ustawienia",
|
||||
"contactPageDescription": "Zarządzaj meta tagami SEO dla strony kontaktu",
|
||||
"metaTitle": "Tytuł Meta",
|
||||
"metaTitlePlaceholder": "Wprowadź tytuł strony dla wyszukiwarek...",
|
||||
"metaDescription": "Opis Meta",
|
||||
"metaDescriptionPlaceholder": "Wprowadź opis strony dla wyszukiwarek...",
|
||||
"metaKeywords": "Słowa Kluczowe Meta",
|
||||
"metaKeywordsPlaceholder": "Wprowadź słowa kluczowe oddzielone przecinkami...",
|
||||
"ogImage": "Obraz Open Graph",
|
||||
"ogImagePlaceholder": "Wprowadź adres URL obrazka do udostępniania w mediach społecznościowych..."
|
||||
},
|
||||
"blog": {
|
||||
"title": "Blog",
|
||||
"posts": "Artykuły",
|
||||
"pageSettings": "Ustawienia Strony",
|
||||
"postsManagement": "Zarządzanie Artykułami",
|
||||
"pageMetaTags": "Meta Tagi Strony",
|
||||
"newPost": "Nowy Artykuł",
|
||||
"noPosts": "Brak dostępnych artykułów",
|
||||
"searchPosts": "Szukaj artykułów...",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten artykuł?",
|
||||
"published": "Opublikowany",
|
||||
"draft": "Szkic",
|
||||
"neverPublished": "Nigdy nie opublikowany",
|
||||
"category": "Kategoria",
|
||||
"noCategory": "Brak kategorii",
|
||||
"status": "Status",
|
||||
"publishedAt": "Data Publikacji",
|
||||
"views": "Wyświetlenia",
|
||||
"actions": "Akcje",
|
||||
"view": "Zobacz",
|
||||
"edit": "Edytuj",
|
||||
"delete": "Usuń",
|
||||
"basicInfo": "Podstawowe Informacje",
|
||||
"titlePlaceholder": "Wprowadź tytuł artykułu...",
|
||||
"slug": "Slug",
|
||||
"slugPlaceholder": "tytul-mojego-artykulu",
|
||||
"content": "Treść",
|
||||
"contentPlaceholder": "Napisz treść artykułu...",
|
||||
"excerpt": "Streszczenie",
|
||||
"excerptPlaceholder": "Krótki opis artykułu...",
|
||||
"publishSettings": "Ustawienia Publikacji",
|
||||
"selectCategory": "Wybierz kategorię",
|
||||
"publish": "Opublikuj",
|
||||
"publishDate": "Data Publikacji",
|
||||
"featured": "Artykuł Wyróżniony",
|
||||
"createPost": "Utwórz Artykuł",
|
||||
"postNotFound": "Artykuł nie został znaleziony",
|
||||
"postInfo": "Informacje o Artykule",
|
||||
"polishVersion": "Wersja Polska",
|
||||
"englishVersion": "Wersja Angielska",
|
||||
"noContent": "Brak dostępnej treści",
|
||||
"editPost": "Edytuj Artykuł",
|
||||
"updatePost": "Aktualizuj Artykuł",
|
||||
"viewPost": "Zobacz Artykuł",
|
||||
"categories": "Kategorie",
|
||||
"categoriesManagement": "Zarządzanie Kategoriami",
|
||||
"addCategory": "Dodaj Kategorię",
|
||||
"editCategory": "Edytuj Kategorię",
|
||||
"deleteCategory": "Usuń Kategorię",
|
||||
"deleteCategoryConfirm": "Czy na pewno chcesz usunąć tę kategorię? Artykuły w tej kategorii pozostaną, ale bez przypisania do kategorii.",
|
||||
"categoryNamePl": "Nazwa Kategorii (PL)",
|
||||
"categoryNameEn": "Nazwa Kategorii (EN)",
|
||||
"categorySlug": "Slug Kategorii",
|
||||
"categoryDescriptionPl": "Opis (PL)",
|
||||
"categoryDescriptionEn": "Opis (EN)",
|
||||
"categoryActive": "Aktywna",
|
||||
"iconName": "Nazwa Ikony",
|
||||
"iconProvider": "Dostawca Ikony",
|
||||
"order": "Kolejność",
|
||||
"noCategories": "Nie znaleziono kategorii",
|
||||
"subtitle": "Artykuły o programowaniu, technologii i rozwoju oprogramowania",
|
||||
"searchPlaceholder": "Szukaj artykułów...",
|
||||
"allCategories": "Wszystkie",
|
||||
"featuredArticles": "Wyróżnione artykuły",
|
||||
"allArticles": "Wszystkie artykuły",
|
||||
"noArticlesFound": "Nie znaleziono artykułów spełniających kryteria wyszukiwania.",
|
||||
"relatedArticles": "Powiązane artykuły",
|
||||
"backToBlog": "Powrót do bloga",
|
||||
"articleNotFound": "Artykuł nie znaleziony",
|
||||
"readMore": "Czytaj więcej",
|
||||
"rssFeed": "Kanał RSS"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Komentarze",
|
||||
"description": "Zarządzaj komentarzami na blogu - zatwierdzaj, odrzucaj i usuwaj komentarze",
|
||||
"searchPlaceholder": "Szukaj po autorze, treści lub tytule artykułu...",
|
||||
"statusUpdated": "Status komentarza został zaktualizowany",
|
||||
"updateError": "Wystąpił błąd podczas aktualizacji statusu komentarza",
|
||||
"deleted": "Komentarz został usunięty",
|
||||
"deleteError": "Wystąpił błąd podczas usuwania komentarza",
|
||||
"confirmDelete": "Czy na pewno chcesz usunąć ten komentarz? Ta akcja jest nieodwracalna.",
|
||||
"deleteTitle": "Usuwanie komentarza",
|
||||
"deleteMessage": "Czy na pewno chcesz usunąć komentarz od {authorName}? Ta akcja jest nieodwracalna.",
|
||||
"pending": "Oczekujące",
|
||||
"approved": "Zatwierdzone",
|
||||
"total": "Łącznie",
|
||||
"all": "Wszystkie",
|
||||
"statusPending": "Oczekujący",
|
||||
"statusApproved": "Zatwierdzony",
|
||||
"approve": "Zatwierdź",
|
||||
"reject": "Odrzuć",
|
||||
"delete": "Usuń",
|
||||
"article": "Artykuł:",
|
||||
"replyToComment": "Odpowiedź na komentarz",
|
||||
"replyTo": "Odpowiedź na:",
|
||||
"noCommentsFound": "Nie znaleziono komentarzy pasujących do wyszukiwania",
|
||||
"noComments": "Brak komentarzy",
|
||||
"previous": "Poprzednia",
|
||||
"next": "Następna",
|
||||
"addComment": "Dodaj komentarz",
|
||||
"reply": "Odpowiedź",
|
||||
"replyButton": "Odpowiedz",
|
||||
"replyForm": "Odpowiedz na komentarz",
|
||||
"moderationNotice": "Twój komentarz zostanie opublikowany po zatwierdzeniu przez moderatora.",
|
||||
"loadError": "Nie można załadować komentarzy - nieprawidłowy identyfikator artykułu."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "Ładowanie...",
|
||||
"connected": "Połączono",
|
||||
"disconnected": "Rozłączono",
|
||||
"checking": "Sprawdzanie...",
|
||||
"save": "Zapisz",
|
||||
"saving": "Zapisywanie...",
|
||||
"saved": "Zmiany zostały zapisane!",
|
||||
"add": "Dodaj",
|
||||
"addImage": "Dodaj obraz",
|
||||
"edit": "Edytuj",
|
||||
"update": "Aktualizuj",
|
||||
"delete": "Usuń",
|
||||
"cancel": "Anuluj",
|
||||
"actions": "Akcje",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
"updating": "Aktualizowanie...",
|
||||
"updated": "Zaktualizowano pomyślnie",
|
||||
"deleting": "Usuwanie...",
|
||||
"deleted": "Usunięto pomyślnie",
|
||||
"select": "Wybierz",
|
||||
"search": "Szukaj",
|
||||
"changeOrder": "Tryb zmiany kolejności",
|
||||
"back": "Wstecz",
|
||||
"untitled": "Bez tytułu",
|
||||
"draft": "Szkic"
|
||||
},
|
||||
"fileUpload": {
|
||||
"uploading": "Przesyłanie...",
|
||||
"dragAndDrop": "Przeciągnij i upuść obraz tutaj, lub",
|
||||
"chooseFile": "Wybierz plik",
|
||||
"clear": "Wyczyść",
|
||||
"selectOrUploadImage": "Wybierz lub prześlij obraz"
|
||||
},
|
||||
"welcome": "Witaj",
|
||||
"login": {
|
||||
"title": "Panel Administracyjny",
|
||||
"email": "E-mail",
|
||||
"password": "Hasło",
|
||||
"signIn": "Zaloguj się",
|
||||
"success": "Logowanie udane",
|
||||
"submit": "Zaloguj się",
|
||||
"noAccount": "Nie masz jeszcze konta?",
|
||||
"register": "Zarejestruj się"
|
||||
},
|
||||
"register": {
|
||||
"title": "Rejestracja",
|
||||
"success": "Rejestracja udana",
|
||||
"submit": "Zarejestruj się",
|
||||
"haveAccount": "Masz już konto?",
|
||||
"signIn": "Zaloguj się",
|
||||
"name": "Imię i nazwisko",
|
||||
"login": "Logowanie"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panel administracyjny",
|
||||
"welcome": "Witaj ponownie",
|
||||
"welcomeUser": "Witaj {name}",
|
||||
"privateData": "Prywatne dane: {{message}}",
|
||||
"nav": {
|
||||
"homepage": "Strona główna",
|
||||
"skills": "Umiejętności",
|
||||
"projects": "Projekty",
|
||||
"navigation": "Nawigacja",
|
||||
"topbar": "Górny pasek",
|
||||
"contact": "Kontakt",
|
||||
"blog": "Blog",
|
||||
"privacyPolicy": "Polityka Prywatności",
|
||||
"newsletter": "Newsletter"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Powrót do strony",
|
||||
"dashboard": "Panel",
|
||||
"settings": "Ustawienia",
|
||||
"keyboardHint": "Możesz użyć klawiszy strzałkowych do nawigacji",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"theme": {
|
||||
"toggle": "Przełącz motyw",
|
||||
"light": "Jasny",
|
||||
"dark": "Ciemny",
|
||||
"system": "Systemowy"
|
||||
},
|
||||
"footer": {
|
||||
"copyright": "Wszelkie prawa zastrzeżone"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Moje projekty",
|
||||
"noProjects": "Brak projektów do wyświetlenia",
|
||||
"github": "GitHub",
|
||||
"gitea": "Gitea",
|
||||
"demo": "Demo"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Umiejętności",
|
||||
"noSkills": "Brak umiejętności w tej kategorii",
|
||||
"noCategories": "Brak kategorii umiejętności"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Kontakt",
|
||||
"directContact": "Dane kontaktowe",
|
||||
"quickMessage": "Wyślij wiadomość",
|
||||
"form": {
|
||||
"name": "Imię i nazwisko",
|
||||
"email": "Adres e-mail",
|
||||
"message": "Wiadomość",
|
||||
"send": "Wyślij wiadomość",
|
||||
"success": "Wiadomość została wysłana pomyślnie!",
|
||||
"error": "Wystąpił błąd podczas wysyłania wiadomości. Spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"switch": "Zmień język"
|
||||
},
|
||||
"userMenu": {
|
||||
"signIn": "Zaloguj się",
|
||||
"signOut": "Wyloguj się",
|
||||
"myAccount": "Moje konto"
|
||||
},
|
||||
"home": {
|
||||
"apiStatus": "Status API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "To pole jest wymagane",
|
||||
"min": "To pole musi mieć co najmniej {min} znaków",
|
||||
"max": "To pole musi mieć co najwyżej {max} znaków",
|
||||
"minLength": "To pole musi mieć co najmniej {min} znaków",
|
||||
"maxLength": "To pole musi mieć co najwyżej {max} znaków",
|
||||
"minValue": "To pole musi mieć co najmniej {min} wartość",
|
||||
"maxValue": "To pole musi mieć co najwyżej {max} wartość",
|
||||
"invalid": "To pole jest nieprawidłowe",
|
||||
"invalidUrl": "Wprowadź prawidłowy adres URL (http:// lub https://)",
|
||||
"turnstileRequired": "Proszę ukończyć weryfikację bezpieczeństwa"
|
||||
},
|
||||
"blog": {
|
||||
"title": "Blog",
|
||||
"subtitle": "Artykuły o programowaniu, technologii i rozwoju oprogramowania",
|
||||
"searchPlaceholder": "Szukaj artykułów...",
|
||||
"allCategories": "Wszystkie",
|
||||
"featuredArticles": "Wyróżnione artykuły",
|
||||
"allArticles": "Wszystkie artykuły",
|
||||
"noArticlesFound": "Nie znaleziono artykułów spełniających kryteria wyszukiwania.",
|
||||
"relatedArticles": "Powiązane artykuły",
|
||||
"backToBlog": "Powrót do bloga",
|
||||
"articleNotFound": "Artykuł nie znaleziony",
|
||||
"readMore": "Czytaj więcej",
|
||||
"featured": "Wyróżniony",
|
||||
"views": "wyświetleń",
|
||||
"rssFeed": "Kanał RSS",
|
||||
"share": {
|
||||
"title": "Udostępnij artykuł",
|
||||
"facebook": "Udostępnij na Facebook",
|
||||
"twitter": "Udostępnij na Twitter",
|
||||
"linkedin": "Udostępnij na LinkedIn",
|
||||
"whatsapp": "Udostępnij przez WhatsApp",
|
||||
"email": "Udostępnij przez e-mail",
|
||||
"copyLink": "Skopiuj link",
|
||||
"linkCopied": "Link skopiowany do schowka!"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Strona nie znaleziona",
|
||||
"description": "Szukana strona nie została znaleziona. Możliwe, że została przeniesiona lub usunięta.",
|
||||
"homepage": "Strona główna"
|
||||
},
|
||||
"newsletter": {
|
||||
"title": "Newsletter",
|
||||
"description": "Otrzymuj powiadomienia o nowych artykułach",
|
||||
"email": "Adres e-mail",
|
||||
"emailPlaceholder": "twoj@email.com",
|
||||
"language": "Język newslettera",
|
||||
"gdprConsent": "Wyrażam zgodę na przetwarzanie danych w celu otrzymywania newslettera zgodnie z",
|
||||
"gdprConsentCompact": "Wyrażam zgodę na przetwarzanie moich danych osobowych w celu otrzymywania newslettera.",
|
||||
"gdprConsentError": "Musisz wyrazić zgodę na przetwarzanie danych",
|
||||
"privacyPolicy": "polityką prywatności",
|
||||
"subscribe": "Zapisz się do newslettera",
|
||||
"subscribeShort": "Zapisz się",
|
||||
"subscribing": "Zapisywanie...",
|
||||
"successMessage": "Pomyślnie zapisano do newslettera!",
|
||||
"errorMessage": "Wystąpił błąd podczas zapisywania",
|
||||
"blogSectionTitle": "Zapisz się do newslettera",
|
||||
"blogSectionDescription": "Otrzymuj powiadomienia o nowych artykułach bezpośrednio na swoją skrzynkę e-mail",
|
||||
"articleSectionTitle": "Podobał Ci się artykuł?",
|
||||
"articleSectionDescription": "Zapisz się do newslettera, aby nie przegapić kolejnych!",
|
||||
"contentPlaceholders": {
|
||||
"privacyPolicyPl": "Wprowadź treść polityki prywatności w języku polskim...",
|
||||
"privacyPolicyEn": "Wprowadź treść polityki prywatności w języku angielskim..."
|
||||
},
|
||||
"languages": {
|
||||
"polish": "Polski",
|
||||
"english": "Angielski"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Zarządzanie Newsletterem",
|
||||
"description": "Zarządzaj subskrypcjami newslettera i wysyłaj powiadomienia do subskrybentów",
|
||||
"stats": {
|
||||
"title": "Statystyki Newslettera",
|
||||
"description": "Aktualna liczba subskrybentów według języka",
|
||||
"polish": "Subskrybenci Polscy",
|
||||
"english": "Subskrybenci Angielscy",
|
||||
"total": "Wszyscy Subskrybenci",
|
||||
"loading": "Ładowanie statystyk..."
|
||||
},
|
||||
"send": {
|
||||
"title": "Wyślij Newsletter",
|
||||
"description": "Wyślij newsletter do subskrybentów o opublikowanym artykule",
|
||||
"selectArticle": "Wybierz Artykuł",
|
||||
"articlePlaceholder": "Wybierz artykuł do wyróżnienia...",
|
||||
"preview": "Podgląd Wybranego Artykułu:",
|
||||
"polishTitle": "Tytuł Polski:",
|
||||
"englishTitle": "Tytuł Angielski:",
|
||||
"slug": "Slug:",
|
||||
"status": "Status:",
|
||||
"published": "Opublikowany",
|
||||
"draft": "Szkic",
|
||||
"notAvailable": "Niedostępne",
|
||||
"sendToPolish": "Wyślij do Polskich Subskrybentów",
|
||||
"sendToEnglish": "Wyślij do Angielskich Subskrybentów",
|
||||
"confirmTitle": "Potwierdź Wysyłanie Newslettera",
|
||||
"confirmDescription": "Ta akcja jest nieodwracalna. Newsletter zostanie wysłany natychmiast do wszystkich subskrybentów.",
|
||||
"confirmDetails": {
|
||||
"article": "Artykuł:",
|
||||
"language": "Język:",
|
||||
"recipients": "Odbiorcy:",
|
||||
"subscribers": "subskrybentów"
|
||||
},
|
||||
"cancel": "Anuluj",
|
||||
"sending": "Wysyłanie...",
|
||||
"confirm": "Tak, Wyślij Newsletter",
|
||||
"selectArticleFirst": "Najpierw wybierz artykuł",
|
||||
"successMessage": "Newsletter wysłany do {count} {language} subskrybentów",
|
||||
"polish": "polskich",
|
||||
"english": "angielskich",
|
||||
"unsubscribe": {
|
||||
"title": "Wypisz się z newslettera",
|
||||
"subtitle": "Przykro nam, że odchodzisz. Twoja opinia pomoże nam się poprawić.",
|
||||
"emailAddress": "Adres email",
|
||||
"emailPlaceholder": "twoj@email.com",
|
||||
"reason": "Powód wypisania się (opcjonalnie)",
|
||||
"reasons": {
|
||||
"too_frequent": "Zbyt częste wiadomości",
|
||||
"not_relevant": "Treści nie są dla mnie odpowiednie",
|
||||
"never_subscribed": "Nigdy się nie zapisałem/am",
|
||||
"poor_content": "Niska jakość treści",
|
||||
"technical_issues": "Problemy techniczne",
|
||||
"other": "Inny powód"
|
||||
},
|
||||
"feedback": "Dodatkowe uwagi",
|
||||
"feedbackPlaceholder": "Powiedz nam więcej o swoich uwagach...",
|
||||
"cancel": "Anuluj",
|
||||
"unsubscribeAction": "Wypisz się",
|
||||
"unsubscribing": "Wypisywanie...",
|
||||
"emailRequired": "Adres email jest wymagany",
|
||||
"successTitle": "Wypisano z newslettera",
|
||||
"successMessage": "Zostałeś pomyślnie wypisany z newslettera BWitek.dev. Dziękujemy za opinię!",
|
||||
"feedbackNote": "Twoja opinia pomaga nam lepiej dostosować nasze treści do potrzeb czytelników.",
|
||||
"home": "Strona główna",
|
||||
"blog": "Blog"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"comments": {
|
||||
"title": "Komentarze",
|
||||
"addComment": "Dodaj komentarz",
|
||||
"reply": "Odpowiedź",
|
||||
"replyButton": "Odpowiedz",
|
||||
"replyForm": "Odpowiedz na komentarz",
|
||||
"noComments": "Brak komentarzy. Bądź pierwszy!",
|
||||
"moderationNotice": "Twój komentarz zostanie opublikowany po zatwierdzeniu przez moderatora.",
|
||||
"loadError": "Nie można załadować komentarzy - nieprawidłowy identyfikator artykułu.",
|
||||
"form": {
|
||||
"name": "Imię i nazwisko",
|
||||
"namePlaceholder": "Wprowadź swoje imię i nazwisko",
|
||||
"email": "Adres e-mail",
|
||||
"emailPlaceholder": "Wprowadź swój adres e-mail (nie będzie publiczny)",
|
||||
"website": "Strona internetowa",
|
||||
"websitePlaceholder": "https://twoja-strona.pl (opcjonalnie)",
|
||||
"content": "Treść komentarza",
|
||||
"contentPlaceholder": "Napisz swój komentarz...",
|
||||
"submit": "Dodaj komentarz",
|
||||
"success": "Komentarz został dodany i oczekuje na zatwierdzenie!",
|
||||
"error": "Wystąpił błąd podczas dodawania komentarza. Spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"richEditor": {
|
||||
"toolbar": {
|
||||
"bold": "Pogrubienie",
|
||||
"italic": "Kursywa",
|
||||
"strikethrough": "Przekreślenie",
|
||||
"code": "Kod wbudowany",
|
||||
"heading1": "Nagłówek 1",
|
||||
"heading2": "Nagłówek 2",
|
||||
"heading3": "Nagłówek 3",
|
||||
"bulletList": "Lista punktowana",
|
||||
"orderedList": "Lista numerowana",
|
||||
"quote": "Cytat",
|
||||
"addLink": "Dodaj link",
|
||||
"addImage": "Dodaj obraz",
|
||||
"addYoutube": "Dodaj wideo YouTube",
|
||||
"addCodeBlock": "Dodaj blok kodu",
|
||||
"undo": "Cofnij",
|
||||
"redo": "Ponów"
|
||||
},
|
||||
"modals": {
|
||||
"link": {
|
||||
"title": "Dodaj Link",
|
||||
"description": "Wprowadź URL strony do której chcesz linkować. Pozostaw puste aby usunąć link.",
|
||||
"urlLabel": "URL",
|
||||
"urlPlaceholder": "https://example.com",
|
||||
"cancel": "Anuluj",
|
||||
"addLink": "Dodaj Link",
|
||||
"removeLink": "Usuń Link"
|
||||
},
|
||||
"youtube": {
|
||||
"title": "Dodaj Video YouTube",
|
||||
"description": "Wprowadź link do filmu YouTube w jednym z obsługiwanych formatów.",
|
||||
"urlLabel": "URL YouTube",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"supportedFormats": "Obsługiwane formaty:",
|
||||
"formats": [
|
||||
"https://www.youtube.com/watch?v=VIDEO_ID",
|
||||
"https://youtu.be/VIDEO_ID",
|
||||
"https://www.youtube.com/embed/VIDEO_ID"
|
||||
],
|
||||
"invalidUrl": "Nieprawidłowy format URL YouTube",
|
||||
"cancel": "Anuluj",
|
||||
"addVideo": "Dodaj Video"
|
||||
},
|
||||
"codeBlock": {
|
||||
"title": "Dodaj Blok Kodu",
|
||||
"description": "Wprowadź język programowania dla podświetlania składni (opcjonalnie).",
|
||||
"languageLabel": "Język programowania",
|
||||
"languagePlaceholder": "javascript, python, css...",
|
||||
"popularLanguages": "Popularne języki:",
|
||||
"languagesList": "javascript, typescript, python, css, html, json, sql, bash, php, java, cpp",
|
||||
"noHighlighting": "Pozostaw puste dla zwykłego tekstu bez podświetlania.",
|
||||
"cancel": "Anuluj",
|
||||
"addCodeBlock": "Dodaj Blok Kodu"
|
||||
},
|
||||
"image": {
|
||||
"title": "Dodaj Obraz",
|
||||
"description": "Wybierz lub wgraj nowy obraz do artykułu.",
|
||||
"cancel": "Anuluj"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
239
apps/web/messages/pl/admin.json
Normal file
239
apps/web/messages/pl/admin.json
Normal file
@@ -0,0 +1,239 @@
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "Panel Administracyjny",
|
||||
"description": "Witaj w panelu administracyjnym. Wybierz sekcję, którą chcesz edytować."
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Strona Główna",
|
||||
"description": "Edytuj zawartość strony głównej",
|
||||
"welcomeText": "Tekst powitalny",
|
||||
"welcomePlaceholder": "Wprowadź tekst powitalny...",
|
||||
"specializationText": "Specjalizacja",
|
||||
"specializationPlaceholder": "Wprowadź tekst o specjalizacji...",
|
||||
"aboutMeText": "O mnie",
|
||||
"aboutMePlaceholder": "Wprowadź tekst o sobie...",
|
||||
"updateSuccess": "Pomyślnie zaktualizowano stronę główną"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Umiejętności",
|
||||
"addNew": "Dodaj umiejętność",
|
||||
"addNewTitle": "Dodaj nową umiejętność",
|
||||
"editTitle": "Edytuj umiejętność",
|
||||
"namePl": "Nazwa (PL)",
|
||||
"nameEn": "Nazwa (EN)",
|
||||
"categoryPl": "Kategoria (PL)",
|
||||
"categoryEn": "Kategoria (EN)",
|
||||
"category": "Kategoria",
|
||||
"selectCategory": "Wybierz kategorię",
|
||||
"categoriesTitle": "Kategorie umiejętności",
|
||||
"addNewCategory": "Dodaj kategorię",
|
||||
"addNewCategoryTitle": "Dodaj nową kategorię",
|
||||
"editCategoryTitle": "Edytuj kategorię",
|
||||
"deleteCategoryTitle": "Usuwanie kategorii",
|
||||
"deleteCategoryConfirm": "Czy na pewno chcesz usunąć tę kategorię? Umiejętności przypisane do tej kategorii nie będą przypisane do żadnej kategorii.",
|
||||
"icon": "Ikona",
|
||||
"iconName": "Nazwa ikony",
|
||||
"iconProvider": "Dostawca ikony",
|
||||
"order": "Kolejność",
|
||||
"active": "Aktywny",
|
||||
"deleteTitle": "Usuwanie umiejętności",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć tę umiejętność?"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projekty",
|
||||
"addNew": "Dodaj projekt",
|
||||
"addNewTitle": "Dodaj nowy projekt",
|
||||
"editTitle": "Edytuj projekt",
|
||||
"titlePl": "Tytuł (PL)",
|
||||
"titleEn": "Tytuł (EN)",
|
||||
"descriptionPl": "Opis (PL)",
|
||||
"descriptionEn": "Opis (EN)",
|
||||
"url": "Adres URL projektu",
|
||||
"repoUrl": "Adres URL repozytorium (GitHub)",
|
||||
"repoUrl2": "Adres URL repozytorium (Gitea)",
|
||||
"imageUrl": "Adres URL obrazka",
|
||||
"order": "Kolejność",
|
||||
"active": "Aktywny",
|
||||
"deleteTitle": "Usuwanie projektu",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten projekt?"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Kontakt",
|
||||
"addNew": "Dodaj element kontaktu",
|
||||
"addNewTitle": "Dodaj nowy element kontaktu",
|
||||
"editTitle": "Edytuj element kontaktu",
|
||||
"namePl": "Nazwa (PL)",
|
||||
"nameEn": "Nazwa (EN)",
|
||||
"iconName": "Nazwa ikony",
|
||||
"iconProvider": "Dostawca ikony",
|
||||
"url": "URL",
|
||||
"external": "Link zewnętrzny",
|
||||
"newTab": "Otwórz w nowej karcie",
|
||||
"order": "Kolejność",
|
||||
"deleteTitle": "Usuwanie elementu kontaktu",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten element kontaktu?"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Nawigacja",
|
||||
"addNew": "Dodaj element nawigacji",
|
||||
"addNewTitle": "Dodaj nowy element nawigacji",
|
||||
"editTitle": "Edytuj element nawigacji",
|
||||
"labelPl": "Etykieta (PL)",
|
||||
"labelEn": "Etykieta (EN)",
|
||||
"url": "URL",
|
||||
"external": "Link zewnętrzny",
|
||||
"newTab": "Otwórz w nowej karcie",
|
||||
"active": "Aktywny",
|
||||
"order": "Kolejność",
|
||||
"deleteTitle": "Usuwanie elementu nawigacji",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten element nawigacji?"
|
||||
},
|
||||
"topBar": {
|
||||
"title": "Górny pasek",
|
||||
"addNew": "Dodaj element górnego paska",
|
||||
"addNewTitle": "Dodaj nowy element górnego paska",
|
||||
"editTitle": "Edytuj element górnego paska",
|
||||
"namePl": "Nazwa (PL)",
|
||||
"nameEn": "Nazwa (EN)",
|
||||
"iconName": "Nazwa ikony",
|
||||
"iconProvider": "Dostawca ikony",
|
||||
"url": "URL",
|
||||
"external": "Link zewnętrzny",
|
||||
"newTab": "Otwórz w nowej karcie",
|
||||
"order": "Kolejność",
|
||||
"deleteTitle": "Usuwanie elementu górnego paska",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten element górnego paska?"
|
||||
},
|
||||
"metaTags": {
|
||||
"title": "Meta Tagi",
|
||||
"description": "Zarządzaj meta tagami SEO dla strony głównej",
|
||||
"projectsPageTitle": "Ustawienia",
|
||||
"projectsPageDescription": "Zarządzaj meta tagami SEO dla strony projektów",
|
||||
"skillsPageTitle": "Ustawienia",
|
||||
"skillsPageDescription": "Zarządzaj meta tagami SEO dla strony umiejętności",
|
||||
"contactPageTitle": "Ustawienia",
|
||||
"contactPageDescription": "Zarządzaj meta tagami SEO dla strony kontaktu",
|
||||
"metaTitle": "Tytuł Meta",
|
||||
"metaTitlePlaceholder": "Wprowadź tytuł strony dla wyszukiwarek...",
|
||||
"metaDescription": "Opis Meta",
|
||||
"metaDescriptionPlaceholder": "Wprowadź opis strony dla wyszukiwarek...",
|
||||
"metaKeywords": "Słowa Kluczowe Meta",
|
||||
"metaKeywordsPlaceholder": "Wprowadź słowa kluczowe oddzielone przecinkami...",
|
||||
"ogImage": "Obraz Open Graph",
|
||||
"ogImagePlaceholder": "Wprowadź adres URL obrazka do udostępniania w mediach społecznościowych..."
|
||||
},
|
||||
"blog": {
|
||||
"title": "Blog",
|
||||
"posts": "Artykuły",
|
||||
"pageSettings": "Ustawienia Strony",
|
||||
"postsManagement": "Zarządzanie Artykułami",
|
||||
"pageMetaTags": "Meta Tagi Strony",
|
||||
"newPost": "Nowy Artykuł",
|
||||
"noPosts": "Brak dostępnych artykułów",
|
||||
"searchPosts": "Szukaj artykułów...",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć ten artykuł?",
|
||||
"published": "Opublikowany",
|
||||
"draft": "Szkic",
|
||||
"neverPublished": "Nigdy nie opublikowany",
|
||||
"category": "Kategoria",
|
||||
"noCategory": "Brak kategorii",
|
||||
"status": "Status",
|
||||
"publishedAt": "Data Publikacji",
|
||||
"views": "Wyświetlenia",
|
||||
"actions": "Akcje",
|
||||
"view": "Zobacz",
|
||||
"edit": "Edytuj",
|
||||
"delete": "Usuń",
|
||||
"basicInfo": "Podstawowe Informacje",
|
||||
"titlePlaceholder": "Wprowadź tytuł artykułu...",
|
||||
"slug": "Slug",
|
||||
"slugPlaceholder": "tytul-mojego-artykulu",
|
||||
"content": "Treść",
|
||||
"contentPlaceholder": "Napisz treść artykułu...",
|
||||
"excerpt": "Streszczenie",
|
||||
"excerptPlaceholder": "Krótki opis artykułu...",
|
||||
"publishSettings": "Ustawienia Publikacji",
|
||||
"selectCategory": "Wybierz kategorię",
|
||||
"publish": "Opublikuj",
|
||||
"publishDate": "Data Publikacji",
|
||||
"featured": "Artykuł Wyróżniony",
|
||||
"createPost": "Utwórz Artykuł",
|
||||
"postNotFound": "Artykuł nie został znaleziony",
|
||||
"postInfo": "Informacje o Artykule",
|
||||
"polishVersion": "Wersja Polska",
|
||||
"englishVersion": "Wersja Angielska",
|
||||
"noContent": "Brak dostępnej treści",
|
||||
"editPost": "Edytuj Artykuł",
|
||||
"updatePost": "Aktualizuj Artykuł",
|
||||
"viewPost": "Zobacz Artykuł",
|
||||
"categories": "Kategorie",
|
||||
"categoriesManagement": "Zarządzanie Kategoriami",
|
||||
"addCategory": "Dodaj Kategorię",
|
||||
"editCategory": "Edytuj Kategorię",
|
||||
"deleteCategory": "Usuń Kategorię",
|
||||
"deleteCategoryConfirm": "Czy na pewno chcesz usunąć tę kategorię? Artykuły w tej kategorii pozostaną, ale bez przypisania do kategorii.",
|
||||
"categoryNamePl": "Nazwa Kategorii (PL)",
|
||||
"categoryNameEn": "Nazwa Kategorii (EN)",
|
||||
"categorySlug": "Slug Kategorii",
|
||||
"categoryDescriptionPl": "Opis (PL)",
|
||||
"categoryDescriptionEn": "Opis (EN)",
|
||||
"categoryActive": "Aktywna",
|
||||
"iconName": "Nazwa Ikony",
|
||||
"iconProvider": "Dostawca Ikony",
|
||||
"order": "Kolejność",
|
||||
"noCategories": "Nie znaleziono kategorii",
|
||||
"subtitle": "Artykuły o programowaniu, technologii i rozwoju oprogramowania",
|
||||
"searchPlaceholder": "Szukaj artykułów...",
|
||||
"allCategories": "Wszystkie",
|
||||
"featuredArticles": "Wyróżnione artykuły",
|
||||
"allArticles": "Wszystkie artykuły",
|
||||
"noArticlesFound": "Nie znaleziono artykułów spełniających kryteria wyszukiwania.",
|
||||
"relatedArticles": "Powiązane artykuły",
|
||||
"backToBlog": "Powrót do bloga",
|
||||
"articleNotFound": "Artykuł nie znaleziony",
|
||||
"readMore": "Czytaj więcej",
|
||||
"rssFeed": "Kanał RSS"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Komentarze",
|
||||
"description": "Zarządzaj komentarzami na blogu - zatwierdzaj, odrzucaj i usuwaj komentarze",
|
||||
"searchPlaceholder": "Szukaj po autorze, treści lub tytule artykułu...",
|
||||
"statusUpdated": "Status komentarza został zaktualizowany",
|
||||
"updateError": "Wystąpił błąd podczas aktualizacji statusu komentarza",
|
||||
"deleted": "Komentarz został usunięty",
|
||||
"deleteError": "Wystąpił błąd podczas usuwania komentarza",
|
||||
"confirmDelete": "Czy na pewno chcesz usunąć ten komentarz? Ta akcja jest nieodwracalna.",
|
||||
"deleteTitle": "Usuwanie komentarza",
|
||||
"deleteMessage": "Czy na pewno chcesz usunąć komentarz od {authorName}? Ta akcja jest nieodwracalna.",
|
||||
"pending": "Oczekujące",
|
||||
"approved": "Zatwierdzone",
|
||||
"total": "Łącznie",
|
||||
"all": "Wszystkie",
|
||||
"statusPending": "Oczekujący",
|
||||
"statusApproved": "Zatwierdzony",
|
||||
"approve": "Zatwierdź",
|
||||
"reject": "Odrzuć",
|
||||
"delete": "Usuń",
|
||||
"article": "Artykuł:",
|
||||
"replyToComment": "Odpowiedź na komentarz",
|
||||
"replyTo": "Odpowiedź na:",
|
||||
"noCommentsFound": "Nie znaleziono komentarzy pasujących do wyszukiwania",
|
||||
"noComments": "Brak komentarzy",
|
||||
"previous": "Poprzednia",
|
||||
"next": "Następna",
|
||||
"addComment": "Dodaj komentarz",
|
||||
"reply": "Odpowiedź",
|
||||
"replyButton": "Odpowiedz",
|
||||
"replyForm": "Odpowiedz na komentarz",
|
||||
"moderationNotice": "Twój komentarz zostanie opublikowany po zatwierdzeniu przez moderatora.",
|
||||
"loadError": "Nie można załadować komentarzy - nieprawidłowy identyfikator artykułu."
|
||||
},
|
||||
"privacy-policy": {
|
||||
"title": "Polityka Prywatności",
|
||||
"metaTags": "Ustawienia",
|
||||
"metaTagsDescription": "Zarządzaj meta tagami SEO dla strony polityki prywatności",
|
||||
"plContent": "Polska treść",
|
||||
"enContent": "Angielska treść",
|
||||
"content": "Treść Polityki Prywatności"
|
||||
}
|
||||
}
|
||||
26
apps/web/messages/pl/blog.json
Normal file
26
apps/web/messages/pl/blog.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"title": "Blog",
|
||||
"subtitle": "Artykuły o programowaniu, technologii i rozwoju oprogramowania",
|
||||
"searchPlaceholder": "Szukaj artykułów...",
|
||||
"allCategories": "Wszystkie",
|
||||
"featuredArticles": "Wyróżnione artykuły",
|
||||
"allArticles": "Wszystkie artykuły",
|
||||
"noArticlesFound": "Nie znaleziono artykułów spełniających kryteria wyszukiwania.",
|
||||
"relatedArticles": "Powiązane artykuły",
|
||||
"backToBlog": "Powrót do bloga",
|
||||
"articleNotFound": "Artykuł nie znaleziony",
|
||||
"readMore": "Czytaj więcej",
|
||||
"featured": "Wyróżniony",
|
||||
"views": "wyświetleń",
|
||||
"rssFeed": "Kanał RSS",
|
||||
"share": {
|
||||
"title": "Udostępnij artykuł",
|
||||
"facebook": "Udostępnij na Facebook",
|
||||
"twitter": "Udostępnij na Twitter",
|
||||
"linkedin": "Udostępnij na LinkedIn",
|
||||
"whatsapp": "Udostępnij przez WhatsApp",
|
||||
"email": "Udostępnij przez e-mail",
|
||||
"copyLink": "Skopiuj link",
|
||||
"linkCopied": "Link skopiowany do schowka!"
|
||||
}
|
||||
}
|
||||
23
apps/web/messages/pl/comments.json
Normal file
23
apps/web/messages/pl/comments.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"title": "Komentarze",
|
||||
"addComment": "Dodaj komentarz",
|
||||
"reply": "Odpowiedź",
|
||||
"replyButton": "Odpowiedz",
|
||||
"replyForm": "Odpowiedz na komentarz",
|
||||
"noComments": "Brak komentarzy. Bądź pierwszy!",
|
||||
"moderationNotice": "Twój komentarz zostanie opublikowany po zatwierdzeniu przez moderatora.",
|
||||
"loadError": "Nie można załadować komentarzy - nieprawidłowy identyfikator artykułu.",
|
||||
"form": {
|
||||
"name": "Imię i nazwisko",
|
||||
"namePlaceholder": "Wprowadź swoje imię i nazwisko",
|
||||
"email": "Adres e-mail",
|
||||
"emailPlaceholder": "Wprowadź swój adres e-mail (nie będzie publiczny)",
|
||||
"website": "Strona internetowa",
|
||||
"websitePlaceholder": "https://twoja-strona.pl (opcjonalnie)",
|
||||
"content": "Treść komentarza",
|
||||
"contentPlaceholder": "Napisz swój komentarz...",
|
||||
"submit": "Dodaj komentarz",
|
||||
"success": "Komentarz został dodany i oczekuje na zatwierdzenie!",
|
||||
"error": "Wystąpił błąd podczas dodawania komentarza. Spróbuj ponownie."
|
||||
}
|
||||
}
|
||||
33
apps/web/messages/pl/common.json
Normal file
33
apps/web/messages/pl/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"welcome": "Witaj",
|
||||
"loading": "Ładowanie...",
|
||||
"connected": "Połączono",
|
||||
"disconnected": "Rozłączono",
|
||||
"checking": "Sprawdzanie...",
|
||||
"save": "Zapisz",
|
||||
"saving": "Zapisywanie...",
|
||||
"saved": "Zmiany zostały zapisane!",
|
||||
"submitting": "Wysyłanie...",
|
||||
"add": "Dodaj",
|
||||
"addImage": "Dodaj obraz",
|
||||
"edit": "Edytuj",
|
||||
"update": "Aktualizuj",
|
||||
"delete": "Usuń",
|
||||
"cancel": "Anuluj",
|
||||
"actions": "Akcje",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
"updating": "Aktualizowanie...",
|
||||
"updated": "Zaktualizowano pomyślnie",
|
||||
"deleting": "Usuwanie...",
|
||||
"deleted": "Usunięto pomyślnie",
|
||||
"select": "Wybierz",
|
||||
"search": "Szukaj",
|
||||
"changeOrder": "Tryb zmiany kolejności",
|
||||
"back": "Wstecz",
|
||||
"untitled": "Bez tytułu",
|
||||
"draft": "Szkic",
|
||||
"showMore": "Pokaż więcej",
|
||||
"noResults": "Nie znaleziono wyników",
|
||||
"noData": "Brak danych"
|
||||
}
|
||||
107
apps/web/messages/pl/components.json
Normal file
107
apps/web/messages/pl/components.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"fileUpload": {
|
||||
"uploading": "Przesyłanie...",
|
||||
"dragAndDrop": "Przeciągnij i upuść obraz tutaj, lub",
|
||||
"chooseFile": "Wybierz plik",
|
||||
"clear": "Wyczyść",
|
||||
"selectOrUploadImage": "Wybierz lub prześlij obraz"
|
||||
},
|
||||
"richEditor": {
|
||||
"toolbar": {
|
||||
"bold": "Pogrubienie",
|
||||
"italic": "Kursywa",
|
||||
"strikethrough": "Przekreślenie",
|
||||
"code": "Kod wbudowany",
|
||||
"heading1": "Nagłówek 1",
|
||||
"heading2": "Nagłówek 2",
|
||||
"heading3": "Nagłówek 3",
|
||||
"bulletList": "Lista punktowana",
|
||||
"orderedList": "Lista numerowana",
|
||||
"quote": "Cytat",
|
||||
"addLink": "Dodaj link",
|
||||
"addImage": "Dodaj obraz",
|
||||
"addYoutube": "Dodaj wideo YouTube",
|
||||
"addCodeBlock": "Dodaj blok kodu",
|
||||
"undo": "Cofnij",
|
||||
"redo": "Ponów"
|
||||
},
|
||||
"modals": {
|
||||
"link": {
|
||||
"title": "Dodaj Link",
|
||||
"description": "Wprowadź URL strony do której chcesz linkować. Pozostaw puste aby usunąć link.",
|
||||
"urlLabel": "URL",
|
||||
"urlPlaceholder": "https://example.com",
|
||||
"cancel": "Anuluj",
|
||||
"addLink": "Dodaj Link",
|
||||
"removeLink": "Usuń Link"
|
||||
},
|
||||
"youtube": {
|
||||
"title": "Dodaj Video YouTube",
|
||||
"description": "Wprowadź link do filmu YouTube w jednym z obsługiwanych formatów.",
|
||||
"urlLabel": "URL YouTube",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"supportedFormats": "Obsługiwane formaty:",
|
||||
"formats": [
|
||||
"https://www.youtube.com/watch?v=VIDEO_ID",
|
||||
"https://youtu.be/VIDEO_ID",
|
||||
"https://www.youtube.com/embed/VIDEO_ID"
|
||||
],
|
||||
"invalidUrl": "Nieprawidłowy format URL YouTube",
|
||||
"cancel": "Anuluj",
|
||||
"addVideo": "Dodaj Video"
|
||||
},
|
||||
"codeBlock": {
|
||||
"title": "Dodaj Blok Kodu",
|
||||
"description": "Wprowadź język programowania dla podświetlania składni (opcjonalnie).",
|
||||
"languageLabel": "Język programowania",
|
||||
"languagePlaceholder": "javascript, python, css...",
|
||||
"popularLanguages": "Popularne języki:",
|
||||
"languagesList": "javascript, typescript, python, css, html, json, sql, bash, php, java, cpp",
|
||||
"noHighlighting": "Pozostaw puste dla zwykłego tekstu bez podświetlania.",
|
||||
"cancel": "Anuluj",
|
||||
"addCodeBlock": "Dodaj Blok Kodu"
|
||||
},
|
||||
"image": {
|
||||
"title": "Dodaj Obraz",
|
||||
"description": "Wybierz lub wgraj nowy obraz do artykułu.",
|
||||
"cancel": "Anuluj"
|
||||
}
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"switch": "Zmień język"
|
||||
},
|
||||
"userMenu": {
|
||||
"signIn": "Zaloguj się",
|
||||
"signOut": "Wyloguj się",
|
||||
"myAccount": "Moje konto"
|
||||
},
|
||||
"theme": {
|
||||
"toggle": "Przełącz motyw",
|
||||
"light": "Jasny",
|
||||
"dark": "Ciemny",
|
||||
"system": "Systemowy"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Powrót do strony",
|
||||
"dashboard": "Panel",
|
||||
"settings": "Ustawienia",
|
||||
"keyboardHint": "Możesz użyć klawiszy strzałkowych do nawigacji",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"footer": {
|
||||
"copyright": "Wszelkie prawa zastrzeżone"
|
||||
},
|
||||
"validation": {
|
||||
"required": "To pole jest wymagane",
|
||||
"min": "To pole musi mieć co najmniej {min} znaków",
|
||||
"max": "To pole musi mieć co najwyżej {max} znaków",
|
||||
"minLength": "To pole musi mieć co najmniej {min} znaków",
|
||||
"maxLength": "To pole musi mieć co najwyżej {max} znaków",
|
||||
"minValue": "To pole musi mieć co najmniej {min} wartość",
|
||||
"maxValue": "To pole musi mieć co najwyżej {max} wartość",
|
||||
"invalid": "To pole jest nieprawidłowe",
|
||||
"invalidUrl": "Wprowadź prawidłowy adres URL (http:// lub https://)",
|
||||
"turnstileRequired": "Proszę ukończyć weryfikację bezpieczeństwa"
|
||||
}
|
||||
}
|
||||
97
apps/web/messages/pl/newsletter.json
Normal file
97
apps/web/messages/pl/newsletter.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"title": "Newsletter",
|
||||
"description": "Otrzymuj powiadomienia o nowych artykułach",
|
||||
"email": "Adres e-mail",
|
||||
"emailPlaceholder": "twoj@email.com",
|
||||
"language": "Język newslettera",
|
||||
"gdprConsent": "Wyrażam zgodę na przetwarzanie danych w celu otrzymywania newslettera zgodnie z",
|
||||
"gdprConsentCompact": "Wyrażam zgodę na przetwarzanie moich danych osobowych w celu otrzymywania newslettera.",
|
||||
"gdprConsentError": "Musisz wyrazić zgodę na przetwarzanie danych",
|
||||
"privacyPolicy": "polityką prywatności",
|
||||
"subscribe": "Zapisz się do newslettera",
|
||||
"subscribeShort": "Zapisz się",
|
||||
"subscribing": "Zapisywanie...",
|
||||
"successMessage": "Pomyślnie zapisano do newslettera!",
|
||||
"errorMessage": "Wystąpił błąd podczas zapisywania",
|
||||
"blogSectionTitle": "Zapisz się do newslettera",
|
||||
"blogSectionDescription": "Otrzymuj powiadomienia o nowych artykułach bezpośrednio na swoją skrzynkę e-mail",
|
||||
"articleSectionTitle": "Podobał Ci się artykuł?",
|
||||
"articleSectionDescription": "Zapisz się do newslettera, aby nie przegapić kolejnych!",
|
||||
"contentPlaceholders": {
|
||||
"privacyPolicyPl": "Wprowadź treść polityki prywatności w języku polskim...",
|
||||
"privacyPolicyEn": "Wprowadź treść polityki prywatności w języku angielskim..."
|
||||
},
|
||||
"languages": {
|
||||
"polish": "Polski",
|
||||
"english": "Angielski"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Zarządzanie Newsletterem",
|
||||
"description": "Zarządzaj subskrypcjami newslettera i wysyłaj powiadomienia do subskrybentów",
|
||||
"stats": {
|
||||
"title": "Statystyki Newslettera",
|
||||
"description": "Aktualna liczba subskrybentów według języka",
|
||||
"polish": "Subskrybenci Polscy",
|
||||
"english": "Subskrybenci Angielscy",
|
||||
"total": "Wszyscy Subskrybenci",
|
||||
"loading": "Ładowanie statystyk..."
|
||||
},
|
||||
"send": {
|
||||
"title": "Wyślij Newsletter",
|
||||
"description": "Wyślij newsletter do subskrybentów o opublikowanym artykule",
|
||||
"selectArticle": "Wybierz Artykuł",
|
||||
"articlePlaceholder": "Wybierz artykuł do wyróżnienia...",
|
||||
"preview": "Podgląd Wybranego Artykułu:",
|
||||
"polishTitle": "Tytuł Polski:",
|
||||
"englishTitle": "Tytuł Angielski:",
|
||||
"slug": "Slug:",
|
||||
"status": "Status:",
|
||||
"published": "Opublikowany",
|
||||
"draft": "Szkic",
|
||||
"notAvailable": "Niedostępne",
|
||||
"sendToPolish": "Wyślij do Polskich Subskrybentów",
|
||||
"sendToEnglish": "Wyślij do Angielskich Subskrybentów",
|
||||
"confirmTitle": "Potwierdź Wysyłanie Newslettera",
|
||||
"confirmDescription": "Ta akcja jest nieodwracalna. Newsletter zostanie wysłany natychmiast do wszystkich subskrybentów.",
|
||||
"confirmDetails": {
|
||||
"article": "Artykuł:",
|
||||
"language": "Język:",
|
||||
"recipients": "Odbiorcy:",
|
||||
"subscribers": "subskrybentów"
|
||||
},
|
||||
"cancel": "Anuluj",
|
||||
"sending": "Wysyłanie...",
|
||||
"confirm": "Tak, Wyślij Newsletter",
|
||||
"selectArticleFirst": "Najpierw wybierz artykuł",
|
||||
"successMessage": "Newsletter wysłany do {count} {language} subskrybentów",
|
||||
"polish": "polskich",
|
||||
"english": "angielskich",
|
||||
"unsubscribe": {
|
||||
"title": "Wypisz się z newslettera",
|
||||
"subtitle": "Przykro nam, że odchodzisz. Twoja opinia pomoże nam się poprawić.",
|
||||
"emailAddress": "Adres email",
|
||||
"emailPlaceholder": "twoj@email.com",
|
||||
"reason": "Powód wypisania się (opcjonalnie)",
|
||||
"reasons": {
|
||||
"too_frequent": "Zbyt częste wiadomości",
|
||||
"not_relevant": "Treści nie są dla mnie odpowiednie",
|
||||
"never_subscribed": "Nigdy się nie zapisałem/am",
|
||||
"poor_content": "Niska jakość treści",
|
||||
"technical_issues": "Problemy techniczne",
|
||||
"other": "Inny powód"
|
||||
},
|
||||
"feedback": "Dodatkowe uwagi",
|
||||
"feedbackPlaceholder": "Powiedz nam więcej o swoich uwagach...",
|
||||
"cancel": "Anuluj",
|
||||
"unsubscribeAction": "Wypisz się",
|
||||
"unsubscribing": "Wypisywanie...",
|
||||
"emailRequired": "Adres email jest wymagany",
|
||||
"successTitle": "Wypisano z newslettera",
|
||||
"successMessage": "Zostałeś pomyślnie wypisany z newslettera BWitek.dev. Dziękujemy za opinię!",
|
||||
"feedbackNote": "Twoja opinia pomaga nam lepiej dostosować nasze treści do potrzeb czytelników.",
|
||||
"home": "Strona główna",
|
||||
"blog": "Blog"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
apps/web/messages/pl/pages.json
Normal file
68
apps/web/messages/pl/pages.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Panel Administracyjny",
|
||||
"email": "E-mail",
|
||||
"password": "Hasło",
|
||||
"signIn": "Zaloguj się",
|
||||
"success": "Logowanie udane",
|
||||
"submit": "Zaloguj się",
|
||||
"noAccount": "Nie masz jeszcze konta?",
|
||||
"register": "Zarejestruj się"
|
||||
},
|
||||
"register": {
|
||||
"title": "Rejestracja",
|
||||
"success": "Rejestracja udana",
|
||||
"submit": "Zarejestruj się",
|
||||
"haveAccount": "Masz już konto?",
|
||||
"signIn": "Zaloguj się",
|
||||
"name": "Imię i nazwisko",
|
||||
"login": "Logowanie"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panel administracyjny",
|
||||
"welcome": "Witaj ponownie",
|
||||
"welcomeUser": "Witaj {name}",
|
||||
"privateData": "Prywatne dane: {{message}}",
|
||||
"nav": {
|
||||
"homepage": "Strona główna",
|
||||
"skills": "Umiejętności",
|
||||
"projects": "Projekty",
|
||||
"navigation": "Nawigacja",
|
||||
"topbar": "Górny pasek",
|
||||
"contact": "Kontakt",
|
||||
"blog": "Blog",
|
||||
"privacyPolicy": "Polityka Prywatności",
|
||||
"newsletter": "Newsletter"
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"title": "Moje projekty",
|
||||
"noProjects": "Brak projektów do wyświetlenia",
|
||||
"github": "GitHub",
|
||||
"gitea": "Gitea",
|
||||
"demo": "Demo"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Umiejętności",
|
||||
"noSkills": "Brak umiejętności w tej kategorii",
|
||||
"noCategories": "Brak kategorii umiejętności"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Kontakt",
|
||||
"directContact": "Dane kontaktowe",
|
||||
"quickMessage": "Wyślij wiadomość",
|
||||
"form": {
|
||||
"name": "Imię i nazwisko",
|
||||
"email": "Adres e-mail",
|
||||
"message": "Wiadomość",
|
||||
"send": "Wyślij wiadomość",
|
||||
"success": "Wiadomość została wysłana pomyślnie!",
|
||||
"error": "Wystąpił błąd podczas wysyłania wiadomości. Spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Strona nie znaleziona",
|
||||
"description": "Szukana strona nie została znaleziona. Możliwe, że została przeniesiona lub usunięta.",
|
||||
"homepage": "Strona główna"
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.0",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function EditBlogPostPage() {
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.title.pl || !formData.title.en || !formData.slug) {
|
||||
toast.error(t("validation.required"));
|
||||
toast.error(t("components.validation.required"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ export default function ViewBlogPostPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center justify-between flex-col md:flex-row">
|
||||
<div className="flex items-center gap-4 flex-col md:flex-row">
|
||||
<Link href="/admin/blog">
|
||||
<Button variant="outline" size="sm">
|
||||
<Icon name="ArrowLeft" provider="lu" className="w-4 h-4 mr-2" />
|
||||
|
||||
@@ -245,12 +245,12 @@ export default function CommentsAdminPage() {
|
||||
{filteredComments.map((comment) => (
|
||||
<Card key={comment.id} className="border border-gray-200 dark:border-gray-700">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-col sm:flex-row items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center">
|
||||
<Icon name="User" provider="lu" className="text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{comment.authorName}
|
||||
@@ -259,7 +259,7 @@ export default function CommentsAdminPage() {
|
||||
{comment.isApproved ? t('admin.comments.statusApproved') : t('admin.comments.statusPending')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name="Mail" provider="lu" className="text-red-600 dark:text-red-400" />
|
||||
{comment.authorEmail}
|
||||
@@ -280,13 +280,13 @@ export default function CommentsAdminPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2 sm:mt-0">
|
||||
{!comment.isApproved && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleStatusUpdate(comment.id, true)}
|
||||
disabled={updateStatusMutation.isPending}
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
className="bg-green-600 hover:bg-green-700 text-white w-full sm:w-auto"
|
||||
>
|
||||
<Icon name="Check" provider="lu" className="mr-1" />
|
||||
{t('admin.comments.approve')}
|
||||
@@ -299,6 +299,7 @@ export default function CommentsAdminPage() {
|
||||
variant="outline"
|
||||
onClick={() => handleStatusUpdate(comment.id, false)}
|
||||
disabled={updateStatusMutation.isPending}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Icon name="X" provider="lu" className="mr-1" />
|
||||
{t('admin.comments.reject')}
|
||||
@@ -310,6 +311,7 @@ export default function CommentsAdminPage() {
|
||||
variant="destructive"
|
||||
onClick={() => handleDeleteClick(comment.id, comment.authorName)}
|
||||
disabled={deleteCommentMutation.isPending}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Icon name="Trash" provider="lu" className="mr-1" />
|
||||
{t('admin.comments.delete')}
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function NewBlogPostPage() {
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.title.pl || !formData.title.en || !formData.slug) {
|
||||
toast.error(t("validation.required"));
|
||||
toast.error(t("components.validation.required"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { BlogCategoryForm } from "@/components/admin/blog-category-form";
|
||||
import FileUpload from "@/components/admin/file-upload";
|
||||
import { DataTable, type Column } from "@/components/admin/data-table";
|
||||
import { ResponsiveDataTable, type Column } from "@/components/admin/data-table/index";
|
||||
import DeleteConfirmationModal from "@/components/admin/delete-confirmation-modal";
|
||||
|
||||
type TranslatedField = {
|
||||
@@ -249,35 +249,40 @@ export default function AdminBlogPage() {
|
||||
header: t("admin.blog.categoryNamePl"),
|
||||
render: (item) => item.name?.pl || "Unnamed",
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "name.en",
|
||||
header: t("admin.blog.categoryNameEn"),
|
||||
render: (item) => item.name?.en || "Unnamed",
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "iconName",
|
||||
header: t("admin.blog.iconName"),
|
||||
render: (item) => item.iconName ? `${item.iconName} (${item.iconProvider})` : "-",
|
||||
sortable: true,
|
||||
searchable: false
|
||||
searchable: false,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "isActive",
|
||||
header: t("admin.blog.categoryActive"),
|
||||
render: (item) => item.isActive ? t("common.yes") : t("common.no"),
|
||||
sortable: true,
|
||||
searchable: false
|
||||
searchable: false,
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
key: "order",
|
||||
header: t("admin.blog.order"),
|
||||
render: (item) => item.order,
|
||||
sortable: true,
|
||||
searchable: false
|
||||
searchable: false,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -302,6 +307,58 @@ export default function AdminBlogPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const postColumns: Column[] = [
|
||||
{
|
||||
key: "title",
|
||||
header: t("admin.blog.title"),
|
||||
render: (item) => (
|
||||
<div>
|
||||
<div className="font-medium truncate max-w-[250px]" title={item.title?.pl || item.title?.en || t('common.untitled')}>
|
||||
{item.title?.pl || item.title?.en || t('common.untitled')}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground truncate max-w-[200px]" title={`Slug: ${item.slug}`}>
|
||||
Slug: {item.slug}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "category",
|
||||
header: t("admin.blog.category"),
|
||||
render: (item) => item.category?.name?.pl || item.category?.name?.en || t("admin.blog.noCategory"),
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
header: t("admin.blog.status"),
|
||||
render: (item) => getStatusBadge(item.isPublished),
|
||||
sortable: true,
|
||||
searchable: false,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "publishedAt",
|
||||
header: t("admin.blog.publishedAt"),
|
||||
render: (item) => formatDate(item.publishedAt),
|
||||
sortable: true,
|
||||
searchable: false,
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
key: "views",
|
||||
header: t("admin.blog.views"),
|
||||
render: (item) => item.viewCount || 0,
|
||||
sortable: true,
|
||||
searchable: false,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
const renderPostsTable = () => {
|
||||
if (!blogPosts.data || blogPosts.data.length === 0) {
|
||||
return (
|
||||
@@ -312,67 +369,40 @@ export default function AdminBlogPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("admin.blog.title")}</TableHead>
|
||||
<TableHead>{t("admin.blog.category")}</TableHead>
|
||||
<TableHead>{t("admin.blog.status")}</TableHead>
|
||||
<TableHead>{t("admin.blog.publishedAt")}</TableHead>
|
||||
<TableHead>{t("admin.blog.views")}</TableHead>
|
||||
<TableHead>{t("admin.blog.actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{blogPosts.data.map((post: any) => (
|
||||
<TableRow key={post.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{post.title?.pl || post.title?.en || t('common.untitled')}</div>
|
||||
<div className="text-sm text-muted-foreground">Slug: {post.slug}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{post.category?.name?.pl || post.category?.name?.en || t("admin.blog.noCategory")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getStatusBadge(post.isPublished)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(post.publishedAt)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{post.viewCount || 0}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/admin/blog/${post.id}`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Icon name="Eye" provider="lu" className="w-4 h-4 mr-1" />
|
||||
{t("admin.blog.view")}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/admin/blog/${post.id}/edit`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Icon name="Pencil" provider="lu" className="w-4 h-4 mr-1" />
|
||||
{t("admin.blog.edit")}
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeletePost(post.id, post.title?.pl || post.title?.en || t('common.untitled'))}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Icon name="Trash2" provider="lu" className="w-4 h-4 mr-1" />
|
||||
{t("admin.blog.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<ResponsiveDataTable
|
||||
title=""
|
||||
addButtonText=""
|
||||
columns={postColumns}
|
||||
data={blogPosts.data}
|
||||
showAddButton={false}
|
||||
showOrderButtons={false}
|
||||
searchPlaceholder={t("admin.blog.searchPosts")}
|
||||
customActions={(item: any) => (
|
||||
<div className="flex gap-1 flex-wrap w-full justify-end">
|
||||
<Link href={`/admin/blog/${item.id}`} className="flex-shrink-0">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<Icon name="Eye" provider="lu" className="w-4 h-4 mr-1" />
|
||||
{t("admin.blog.view")}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/admin/blog/${item.id}/edit`} className="flex-shrink-0">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<Icon name="Pencil" provider="lu" className="w-4 h-4 mr-1" />
|
||||
{t("admin.blog.edit")}
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeletePost(item.id, item.title?.pl || item.title?.en || t('common.untitled'))}
|
||||
className="text-red-600 hover:text-red-700 flex-shrink-0"
|
||||
>
|
||||
<Icon name="Trash2" provider="lu" className="w-4 h-4 mr-1" />
|
||||
{t("admin.blog.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -413,7 +443,7 @@ export default function AdminBlogPage() {
|
||||
<TabsContent value="categories">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<DataTable
|
||||
<ResponsiveDataTable
|
||||
title={t("admin.blog.categoriesManagement")}
|
||||
addButtonText={t("admin.blog.addCategory")}
|
||||
columns={categoryColumns}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {useTranslations} from 'next-intl';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { DataTable, type Column } from "@/components/admin/data-table";
|
||||
import { ResponsiveDataTable, type Column } from "@/components/admin/data-table/index";
|
||||
import { ContactForm } from "@/components/admin/contact-form";
|
||||
import { Icon } from '@/components/icon';
|
||||
import FileUpload from "@/components/admin/file-upload";
|
||||
@@ -213,29 +213,45 @@ export default function AdminPanelContact() {
|
||||
{
|
||||
key: "name.pl",
|
||||
header: t("admin.contact.namePl"),
|
||||
render: (item) => item.name.pl,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.name.pl}>
|
||||
{item.name.pl}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "name.en",
|
||||
header: t("admin.contact.nameEn"),
|
||||
render: (item) => item.name.en,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.name.en}>
|
||||
{item.name.en}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
header: t("admin.contact.url"),
|
||||
render: (item) => item.url || "-",
|
||||
render: (item) => (
|
||||
<div className="max-w-[200px] truncate" title={item.url || "-"}>
|
||||
{item.url || "-"}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "order",
|
||||
header: t("admin.contact.order"),
|
||||
render: (item) => item.order,
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -362,7 +378,7 @@ export default function AdminPanelContact() {
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<DataTable
|
||||
<ResponsiveDataTable
|
||||
title={t("admin.contact.title")}
|
||||
addButtonText={t("admin.contact.addNew")}
|
||||
columns={contactColumns}
|
||||
|
||||
@@ -35,24 +35,24 @@ export default function AdminLayout({
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h1 className="text-2xl font-bold">{t("dashboard.title")}</h1>
|
||||
<h1 className="text-2xl font-bold">{t("pages.dashboard.title")}</h1>
|
||||
<Sheet open={isMobileNavOpen} onOpenChange={setIsMobileNavOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="lg:hidden">
|
||||
<Icon name="Menu" provider="lu" className="h-5 w-5" />
|
||||
<span className="sr-only">{t("navigation.menu")}</span>
|
||||
<span className="sr-only">{t("components.navigation.menu")}</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-[280px] p-0">
|
||||
<SheetTitle className="sr-only">
|
||||
{t("navigation.menu")}
|
||||
{t("components.navigation.menu")}
|
||||
</SheetTitle>
|
||||
<AdminNav onNavItemClick={() => setIsMobileNavOpen(false)} isMobile />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{t("dashboard.welcomeUser", { name: session?.user.name || "" })}
|
||||
{t("pages.dashboard.welcomeUser", { name: session?.user.name || "" })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -60,8 +60,8 @@ export default function AdminLayout({
|
||||
<div className="hidden lg:block w-64 flex-shrink-0">
|
||||
<AdminNav />
|
||||
</div>
|
||||
<main className="flex-1 min-w-0 max-w-full overflow-x-auto">
|
||||
<div className="max-w-6xl">
|
||||
<main className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="w-full max-w-full">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useTranslations} from 'next-intl';
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { DataTable, type Column } from "@/components/admin/data-table";
|
||||
import { ResponsiveDataTable, type Column } from "@/components/admin/data-table/index";
|
||||
import { NavigationForm } from "@/components/admin/navigation-form";
|
||||
import { TopBarForm } from "@/components/admin/topbar-form";
|
||||
|
||||
@@ -224,35 +224,52 @@ export default function AdminPanelNavigation() {
|
||||
{
|
||||
key: "label.pl",
|
||||
header: t("admin.navigation.labelPl"),
|
||||
render: (item) => item.label.pl,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.label.pl}>
|
||||
{item.label.pl}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "label.en",
|
||||
header: t("admin.navigation.labelEn"),
|
||||
render: (item) => item.label.en,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.label.en}>
|
||||
{item.label.en}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
header: t("admin.navigation.url"),
|
||||
render: (item) => item.url || "-",
|
||||
render: (item) => (
|
||||
<div className="max-w-[200px] truncate" title={item.url || "-"}>
|
||||
{item.url || "-"}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "isActive",
|
||||
header: t("admin.navigation.active"),
|
||||
render: (item) => item.isActive ? t("common.yes") : t("common.no"),
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
key: "order",
|
||||
header: t("admin.navigation.order"),
|
||||
render: (item) => item.order,
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -260,36 +277,53 @@ export default function AdminPanelNavigation() {
|
||||
{
|
||||
key: "name.pl",
|
||||
header: t("admin.topBar.namePl"),
|
||||
render: (item) => item.name.pl,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.name.pl}>
|
||||
{item.name.pl}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "name.en",
|
||||
header: t("admin.topBar.nameEn"),
|
||||
render: (item) => item.name.en,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.name.en}>
|
||||
{item.name.en}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "iconName",
|
||||
header: t("admin.topBar.iconName"),
|
||||
render: (item) => item.iconName || "-",
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
header: t("admin.topBar.url"),
|
||||
render: (item) => item.url || "-",
|
||||
render: (item) => (
|
||||
<div className="max-w-[200px] truncate" title={item.url || "-"}>
|
||||
{item.url || "-"}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "order",
|
||||
header: t("admin.topBar.order"),
|
||||
render: (item) => item.order,
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -307,7 +341,7 @@ export default function AdminPanelNavigation() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="navigation">
|
||||
<DataTable
|
||||
<ResponsiveDataTable
|
||||
title={t("admin.navigation.title")}
|
||||
addButtonText={t("admin.navigation.addNew")}
|
||||
columns={navigationColumns}
|
||||
@@ -326,7 +360,7 @@ export default function AdminPanelNavigation() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="topbar">
|
||||
<DataTable
|
||||
<ResponsiveDataTable
|
||||
title={t("admin.topBar.title")}
|
||||
addButtonText={t("admin.topBar.addNew")}
|
||||
columns={topBarColumns}
|
||||
|
||||
@@ -169,8 +169,8 @@ export default function AdminPanelPrivacyPolicy() {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold leading-none tracking-tight">Privacy Policy Page Meta Tags</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">SEO settings for privacy policy page</p>
|
||||
<h3 className="text-lg font-semibold leading-none tracking-tight">{t("admin.privacy-policy.metaTags")}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t("admin.privacy-policy.metaTagsDescription")}</p>
|
||||
</div>
|
||||
{isMetaExpanded ? (
|
||||
<Icon name="ChevronUp" provider="lu" className="h-4 w-4" />
|
||||
@@ -279,11 +279,11 @@ export default function AdminPanelPrivacyPolicy() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Privacy Policy Content</CardTitle>
|
||||
<CardTitle>{t("admin.privacy-policy.content")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<Label>Polish Content</Label>
|
||||
<Label>{t("admin.privacy-policy.plContent")}</Label>
|
||||
<RichTextEditor
|
||||
content={privacyPolicyFormData.content.pl}
|
||||
onChange={(content) => handlePrivacyPolicyFieldChange("pl", content)}
|
||||
@@ -292,7 +292,7 @@ export default function AdminPanelPrivacyPolicy() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Label>English Content</Label>
|
||||
<Label>{t("admin.privacy-policy.enContent")}</Label>
|
||||
<RichTextEditor
|
||||
content={privacyPolicyFormData.content.en}
|
||||
onChange={(content) => handlePrivacyPolicyFieldChange("en", content)}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {useTranslations} from 'next-intl';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { DataTable, type Column } from "@/components/admin/data-table";
|
||||
import { ResponsiveDataTable, type Column } from "@/components/admin/data-table/index";
|
||||
import { ProjectForm } from "@/components/admin/project-form";
|
||||
import { Icon } from '@/components/icon';
|
||||
import FileUpload from "@/components/admin/file-upload";
|
||||
@@ -219,35 +219,52 @@ export default function AdminPanelProjects() {
|
||||
{
|
||||
key: "title.pl",
|
||||
header: t("admin.projects.titlePl"),
|
||||
render: (item) => item.title.pl,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.title.pl}>
|
||||
{item.title.pl}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "title.en",
|
||||
header: t("admin.projects.titleEn"),
|
||||
render: (item) => item.title.en,
|
||||
render: (item) => (
|
||||
<div className="max-w-[180px] truncate" title={item.title.en}>
|
||||
{item.title.en}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
header: t("admin.projects.url"),
|
||||
render: (item) => item.url || "-",
|
||||
render: (item) => (
|
||||
<div className="max-w-[200px] truncate" title={item.url || "-"}>
|
||||
{item.url || "-"}
|
||||
</div>
|
||||
),
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "isActive",
|
||||
header: t("admin.projects.active"),
|
||||
render: (item) => item.isActive ? t("common.yes") : t("common.no"),
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
key: "order",
|
||||
header: t("admin.projects.order"),
|
||||
render: (item) => item.order,
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -374,7 +391,7 @@ export default function AdminPanelProjects() {
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<DataTable
|
||||
<ResponsiveDataTable
|
||||
title={t("admin.projects.title")}
|
||||
addButtonText={t("admin.projects.addNew")}
|
||||
columns={projectColumns}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {useTranslations} from 'next-intl';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { DataTable, type Column } from "@/components/admin/data-table";
|
||||
import { ResponsiveDataTable, type Column } from "@/components/admin/data-table/index";
|
||||
import { SkillForm } from "@/components/admin/skill-form";
|
||||
import { SkillCategoryForm } from "@/components/admin/skill-category-form";
|
||||
import { Icon } from '@/components/icon';
|
||||
@@ -308,20 +308,23 @@ export default function AdminPanelSkills() {
|
||||
header: t("admin.skills.namePl"),
|
||||
render: (item) => item.name.pl,
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "name.en",
|
||||
header: t("admin.skills.nameEn"),
|
||||
render: (item) => item.name.en,
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "order",
|
||||
header: t("admin.skills.order"),
|
||||
render: (item) => item.order,
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -331,21 +334,24 @@ export default function AdminPanelSkills() {
|
||||
header: t("admin.skills.namePl"),
|
||||
render: (item) => item.name.pl,
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
key: "name.en",
|
||||
header: t("admin.skills.nameEn"),
|
||||
render: (item) => item.name.en,
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "category",
|
||||
header: t("admin.skills.category"),
|
||||
render: (item) => item.category ? `${item.category.pl} / ${item.category.en}` : "-",
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "icon",
|
||||
@@ -355,19 +361,22 @@ export default function AdminPanelSkills() {
|
||||
return item.iconProvider ? `${item.iconName} (${item.iconProvider})` : item.iconName;
|
||||
},
|
||||
sortable: true,
|
||||
searchable: true
|
||||
searchable: true,
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
key: "isActive",
|
||||
header: t("admin.skills.active"),
|
||||
render: (item) => item.isActive ? t("common.yes") : t("common.no"),
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
key: "order",
|
||||
header: t("admin.skills.order"),
|
||||
render: (item) => item.order,
|
||||
sortable: true
|
||||
sortable: true,
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -496,7 +505,7 @@ export default function AdminPanelSkills() {
|
||||
{/* Skills DataTables */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<DataTable
|
||||
<ResponsiveDataTable
|
||||
title={t("admin.skills.categoriesTitle")}
|
||||
addButtonText={t("admin.skills.addNewCategory")}
|
||||
columns={categoryColumns}
|
||||
@@ -513,7 +522,7 @@ export default function AdminPanelSkills() {
|
||||
showOrderButtons={true}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
<ResponsiveDataTable
|
||||
title={t("admin.skills.title")}
|
||||
addButtonText={t("admin.skills.addNew")}
|
||||
columns={skillColumns}
|
||||
|
||||
@@ -7,10 +7,20 @@ import { trpc } from "@/utils/trpc";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import MainLayout from "@/components/main-layout";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function LoginPage() {
|
||||
const registrationStatus = useQuery(trpc.getRegistrationStatus.queryOptions());
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const authStatus = useQuery(trpc.checkAuthStatus.queryOptions());
|
||||
|
||||
useEffect(() => {
|
||||
if (authStatus.data?.isLoggedIn) {
|
||||
router.push("/admin");
|
||||
}
|
||||
}, [authStatus.data, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!registrationStatus.data?.enabled && isRegistering) {
|
||||
@@ -18,7 +28,7 @@ export default function LoginPage() {
|
||||
}
|
||||
}, [registrationStatus.data?.enabled, isRegistering]);
|
||||
|
||||
if (!registrationStatus.data) {
|
||||
if (!registrationStatus.data || authStatus.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ export default function NotFound() {
|
||||
404
|
||||
</h1>
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-300 mb-4">
|
||||
{t('notFound.title')}
|
||||
{t('pages.notFound.title')}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-8">
|
||||
{t('notFound.description')}
|
||||
{t('pages.notFound.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function NotFound() {
|
||||
<Link href="/">
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<Icon name="Home" provider="lu" className="w-4 h-4" />
|
||||
{t('notFound.homepage')}
|
||||
{t('pages.notFound.homepage')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function AdminHeader() {
|
||||
className="flex items-center gap-2 text-sm font-medium transition-colors hover:text-foreground/80"
|
||||
>
|
||||
<Icon name="House" provider="lu" className="h-5 w-5" />
|
||||
<span className="hidden sm:inline">{t('navigation.home')}</span>
|
||||
<span className="hidden sm:inline">{t('components.navigation.home')}</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -13,12 +13,12 @@ interface AdminNavProps {
|
||||
const navItems = [
|
||||
{
|
||||
path: "/admin",
|
||||
labelKey: "dashboard.nav.homepage",
|
||||
labelKey: "pages.dashboard.nav.homepage",
|
||||
icon: "House"
|
||||
},
|
||||
{
|
||||
path: "/admin/blog",
|
||||
labelKey: "dashboard.nav.blog",
|
||||
labelKey: "pages.dashboard.nav.blog",
|
||||
icon: "FileText"
|
||||
},
|
||||
{
|
||||
@@ -28,32 +28,32 @@ const navItems = [
|
||||
},
|
||||
{
|
||||
path: "/admin/projects",
|
||||
labelKey: "dashboard.nav.projects",
|
||||
labelKey: "pages.dashboard.nav.projects",
|
||||
icon: "Briefcase"
|
||||
},
|
||||
{
|
||||
path: "/admin/skills",
|
||||
labelKey: "dashboard.nav.skills",
|
||||
labelKey: "pages.dashboard.nav.skills",
|
||||
icon: "Award"
|
||||
},
|
||||
{
|
||||
path: "/admin/contact",
|
||||
labelKey: "dashboard.nav.contact",
|
||||
labelKey: "pages.dashboard.nav.contact",
|
||||
icon: "Mail"
|
||||
},
|
||||
{
|
||||
path: "/admin/privacy-policy",
|
||||
labelKey: "dashboard.nav.privacyPolicy",
|
||||
labelKey: "pages.dashboard.nav.privacyPolicy",
|
||||
icon: "Shield"
|
||||
},
|
||||
{
|
||||
path: "/admin/navigation",
|
||||
labelKey: "dashboard.nav.navigation",
|
||||
labelKey: "pages.dashboard.nav.navigation",
|
||||
icon: "Menu"
|
||||
},
|
||||
{
|
||||
path: "/admin/newsletter",
|
||||
labelKey: "dashboard.nav.newsletter",
|
||||
labelKey: "pages.dashboard.nav.newsletter",
|
||||
icon: "Send"
|
||||
},
|
||||
];
|
||||
|
||||
@@ -69,10 +69,10 @@ export const BlogCategoryForm = ({
|
||||
|
||||
const schema = z.object({
|
||||
name: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
slug: z.string().min(1, t("validation.required") as string),
|
||||
slug: z.string().min(1, t("components.validation.required") as string),
|
||||
description: z.object({
|
||||
pl: z.string().optional(),
|
||||
en: z.string().optional(),
|
||||
|
||||
@@ -64,8 +64,8 @@ export const ContactForm = ({
|
||||
|
||||
const schema = z.object({
|
||||
name: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
iconName: z.string().nullable().optional(),
|
||||
iconProvider: z.string().nullable().optional(),
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Icon } from '@/components/icon';
|
||||
|
||||
export type Column = {
|
||||
key: string;
|
||||
header: string;
|
||||
render?: (item: any) => ReactNode;
|
||||
sortable?: boolean;
|
||||
searchable?: boolean;
|
||||
};
|
||||
|
||||
type DataTableProps = {
|
||||
title: string;
|
||||
addButtonText: string;
|
||||
columns: Column[];
|
||||
data: any[];
|
||||
FormComponent: React.ComponentType<{
|
||||
initialData?: any;
|
||||
onSubmit: (data: any) => void;
|
||||
onCancel: () => void;
|
||||
}>;
|
||||
onAdd: (data: any) => void;
|
||||
onEdit?: (id: string, data: any) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
onChangeOrder?: (id: string, direction: "up" | "down") => void;
|
||||
addDialogTitle: string;
|
||||
editDialogTitle: string;
|
||||
deleteDialogTitle: string;
|
||||
deleteDialogConfirmText: string;
|
||||
showOrderButtons?: boolean;
|
||||
idField?: string;
|
||||
};
|
||||
|
||||
export const DataTable = ({
|
||||
title,
|
||||
addButtonText,
|
||||
columns,
|
||||
data,
|
||||
FormComponent,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onChangeOrder,
|
||||
addDialogTitle,
|
||||
editDialogTitle,
|
||||
deleteDialogTitle,
|
||||
deleteDialogConfirmText,
|
||||
showOrderButtons = false,
|
||||
idField = "id",
|
||||
}: DataTableProps) => {
|
||||
const t = useTranslations();
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any | null>(null);
|
||||
const [itemToDelete, setItemToDelete] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [orderingMode, setOrderingMode] = useState(false);
|
||||
const [sortConfig, setSortConfig] = useState<{ key: string, direction: 'asc' | 'desc' } | null>(null);
|
||||
|
||||
const handleAdd = (data: any) => {
|
||||
onAdd(data);
|
||||
setIsAddDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleEdit = (data: any) => {
|
||||
if (editingItem && onEdit) {
|
||||
onEdit(editingItem[idField], data);
|
||||
setEditingItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (itemToDelete && onDelete) {
|
||||
onDelete(itemToDelete);
|
||||
setItemToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = (key: string) => {
|
||||
setSortConfig((prevSortConfig) => {
|
||||
if (!prevSortConfig || prevSortConfig.key !== key) {
|
||||
return { key, direction: 'asc' };
|
||||
}
|
||||
|
||||
if (prevSortConfig.direction === 'asc') {
|
||||
return { key, direction: 'desc' };
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (orderingMode) {
|
||||
setSortConfig(null);
|
||||
setSearchTerm("");
|
||||
}
|
||||
}, [orderingMode]);
|
||||
|
||||
const getNestedValue = (obj: any, path: string) => {
|
||||
if (!obj) return '';
|
||||
|
||||
const column = columns.find(c => c.key === path);
|
||||
if (column && column.render) {
|
||||
const rendered = column.render(obj);
|
||||
|
||||
if (typeof rendered !== 'object') {
|
||||
return rendered;
|
||||
}
|
||||
}
|
||||
|
||||
const keys = path.split('.');
|
||||
let value = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (value === null || value === undefined) return '';
|
||||
value = value[key];
|
||||
}
|
||||
|
||||
return value === null || value === undefined ? '' : value;
|
||||
};
|
||||
|
||||
const filteredAndSortedData = useMemo(() => {
|
||||
let result = data;
|
||||
|
||||
if (searchTerm) {
|
||||
const lowerCaseSearchTerm = searchTerm.toLowerCase();
|
||||
result = data.filter((item) => {
|
||||
return columns.some((column) => {
|
||||
if (!column.searchable) return false;
|
||||
|
||||
const value = getNestedValue(item, column.key);
|
||||
return String(value || '').toLowerCase().includes(lowerCaseSearchTerm);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (sortConfig && !orderingMode) {
|
||||
result = [...result].sort((a, b) => {
|
||||
const aValue = getNestedValue(a, sortConfig.key);
|
||||
const bValue = getNestedValue(b, sortConfig.key);
|
||||
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
const aString = String(aValue || '');
|
||||
const bString = String(bValue || '');
|
||||
|
||||
return sortConfig.direction === 'asc'
|
||||
? aString.localeCompare(bString)
|
||||
: bString.localeCompare(aString);
|
||||
});
|
||||
} else if (orderingMode) {
|
||||
result = [...result].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [data, searchTerm, columns, sortConfig, orderingMode]);
|
||||
|
||||
return (
|
||||
<Card className="mt-4">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{addButtonText}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{addDialogTitle}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<FormComponent
|
||||
onSubmit={handleAdd}
|
||||
onCancel={() => setIsAddDialogOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className="px-6 pb-2 flex flex-col sm:flex-row justify-between gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
{showOrderButtons && onChangeOrder && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="ordering-mode"
|
||||
checked={orderingMode}
|
||||
onCheckedChange={setOrderingMode}
|
||||
/>
|
||||
<Label htmlFor="ordering-mode" className="text-sm font-medium">
|
||||
{t("common.changeOrder")}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder={t("common.search")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
disabled={orderingMode}
|
||||
/>
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto max-w-full">
|
||||
<div className="rounded-md border">
|
||||
<Table className="min-w-max">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => {
|
||||
const isTextColumn = column.key.includes('title') || column.key === 'url';
|
||||
return (
|
||||
<TableHead
|
||||
key={column.key}
|
||||
className={`${isTextColumn ? "min-w-[200px] max-w-[300px]" : "whitespace-nowrap"} ${column.sortable && !orderingMode ? "cursor-pointer hover:bg-muted" : ""}`}
|
||||
onClick={() => column.sortable && !orderingMode && handleSort(column.key)}
|
||||
>
|
||||
<div className="flex items-center select-none">
|
||||
{column.header}
|
||||
{sortConfig && sortConfig.key === column.key && (
|
||||
<span className="ml-1">
|
||||
{sortConfig.direction === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
{(onEdit || onDelete || (onChangeOrder && orderingMode)) && (
|
||||
<TableHead className="select-none whitespace-nowrap">{t("common.actions")}</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredAndSortedData.map((item, index) => (
|
||||
<TableRow key={item[idField]}>
|
||||
{columns.map((column) => {
|
||||
const content = column.render
|
||||
? column.render(item)
|
||||
: item[column.key] || "-";
|
||||
|
||||
const isTextColumn = column.key.includes('title') || column.key === 'url';
|
||||
|
||||
return (
|
||||
<TableCell key={`${item[idField]}-${column.key}`} className={isTextColumn ? "min-w-[200px] max-w-[300px]" : "whitespace-nowrap"}>
|
||||
{isTextColumn ? (
|
||||
<div
|
||||
className="truncate"
|
||||
title={typeof content === 'string' ? content : String(content)}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
{(onEdit || onDelete || onChangeOrder) && (
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<div className="flex gap-2">
|
||||
{onEdit && (
|
||||
<Dialog
|
||||
open={editingItem?.[idField] === item[idField]}
|
||||
onOpenChange={(open) =>
|
||||
setEditingItem(open ? item : null)
|
||||
}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
{t("common.edit")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editDialogTitle}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<FormComponent
|
||||
initialData={item}
|
||||
onSubmit={handleEdit}
|
||||
onCancel={() => setEditingItem(null)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{showOrderButtons && onChangeOrder && orderingMode && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChangeOrder(item[idField], "up")}
|
||||
disabled={item.order === 1}
|
||||
>
|
||||
<Icon name="ArrowUp" provider="lu" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChangeOrder(item[idField], "down")}
|
||||
disabled={item.order === Math.max(...data.map(i => i.order))}
|
||||
>
|
||||
<Icon name="ArrowDown" provider="lu" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Dialog
|
||||
open={itemToDelete === item[idField]}
|
||||
onOpenChange={(open) =>
|
||||
setItemToDelete(open ? item[idField] : null)
|
||||
}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{deleteDialogTitle}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p>{deleteDialogConfirmText}</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setItemToDelete(null)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
171
apps/web/src/components/admin/data-table/data-table-actions.tsx
Normal file
171
apps/web/src/components/admin/data-table/data-table-actions.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Icon } from '@/components/icon';
|
||||
|
||||
export interface EditActionProps {
|
||||
item: any;
|
||||
idField: string;
|
||||
editingItem: any;
|
||||
setEditingItem: (item: any) => void;
|
||||
onEdit?: (id: string, data: any) => void;
|
||||
FormComponent?: React.ComponentType<{
|
||||
initialData?: any;
|
||||
onSubmit: (data: any) => void;
|
||||
onCancel: () => void;
|
||||
}>;
|
||||
editDialogTitle?: string;
|
||||
isMobile?: boolean;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export const EditAction = ({
|
||||
item,
|
||||
idField,
|
||||
editingItem,
|
||||
setEditingItem,
|
||||
onEdit,
|
||||
FormComponent,
|
||||
editDialogTitle,
|
||||
isMobile = false,
|
||||
t
|
||||
}: EditActionProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={editingItem?.[idField] === item[idField]}
|
||||
onOpenChange={(open) =>
|
||||
setEditingItem(open ? item : null)
|
||||
}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={isMobile ? "flex-1 min-w-0 text-xs py-1 h-auto" : ""}
|
||||
>
|
||||
<Icon
|
||||
name="Pencil"
|
||||
provider="lu"
|
||||
className={isMobile ? "w-3 h-3 mr-1" : ""}
|
||||
/>
|
||||
{isMobile && t("common.edit")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editDialogTitle}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{FormComponent && (
|
||||
<FormComponent
|
||||
initialData={item}
|
||||
onSubmit={(data: any) => {
|
||||
if (editingItem && onEdit) {
|
||||
onEdit(editingItem[idField], data);
|
||||
setEditingItem(null);
|
||||
}
|
||||
}}
|
||||
onCancel={() => setEditingItem(null)}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export interface DeleteActionProps {
|
||||
item: any;
|
||||
idField: string;
|
||||
setItemToDelete: (id: string | null) => void;
|
||||
isMobile?: boolean;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export const DeleteAction = ({
|
||||
item,
|
||||
idField,
|
||||
setItemToDelete,
|
||||
isMobile = false,
|
||||
t
|
||||
}: DeleteActionProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={false}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
setItemToDelete(item[idField]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className={isMobile ? "flex-1 min-w-0 text-xs py-1 h-auto" : ""}
|
||||
>
|
||||
<Icon
|
||||
name="Trash"
|
||||
provider="lu"
|
||||
className={isMobile ? "w-3 h-3 mr-1" : ""}
|
||||
/>
|
||||
{isMobile && t("common.delete")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export interface OrderActionsProps {
|
||||
item: any;
|
||||
idField: string;
|
||||
data: any[];
|
||||
onChangeOrder?: (id: string, direction: "up" | "down") => void;
|
||||
isMobile?: boolean;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export const OrderActions = ({
|
||||
item,
|
||||
idField,
|
||||
data,
|
||||
onChangeOrder,
|
||||
isMobile = false,
|
||||
t
|
||||
}: OrderActionsProps) => {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={isMobile ? "flex-1 min-w-0 text-xs py-1 h-auto" : ""}
|
||||
onClick={() => onChangeOrder && onChangeOrder(item[idField], "up")}
|
||||
disabled={item.order === 1}
|
||||
>
|
||||
<Icon
|
||||
name="ArrowUp"
|
||||
provider="lu"
|
||||
className={isMobile ? "w-3 h-3 mr-1" : ""}
|
||||
/>
|
||||
{isMobile && t("common.moveUp")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={isMobile ? "flex-1 min-w-0 text-xs py-1 h-auto" : ""}
|
||||
onClick={() => onChangeOrder && onChangeOrder(item[idField], "down")}
|
||||
disabled={item.order === Math.max(...data.map((i: any) => i.order))}
|
||||
>
|
||||
<Icon
|
||||
name="ArrowDown"
|
||||
provider="lu"
|
||||
className={isMobile ? "w-3 h-3 mr-1" : ""}
|
||||
/>
|
||||
{isMobile && t("common.moveDown")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
126
apps/web/src/components/admin/data-table/desktop-data-table.tsx
Normal file
126
apps/web/src/components/admin/data-table/desktop-data-table.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Icon } from '@/components/icon';
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DesktopDataTableProps } from "./types";
|
||||
import { EditAction, DeleteAction, OrderActions } from "./data-table-actions";
|
||||
|
||||
export const DesktopDataTable = ({
|
||||
columns,
|
||||
data,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
setSortColumn,
|
||||
setSortDirection,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onChangeOrder,
|
||||
editingItem,
|
||||
setEditingItem,
|
||||
setItemToDelete,
|
||||
FormComponent,
|
||||
editDialogTitle,
|
||||
showOrderButtons,
|
||||
orderingMode,
|
||||
idField,
|
||||
t,
|
||||
customActions
|
||||
}: DesktopDataTableProps) => {
|
||||
const handleSort = (column: string) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<div className="relative w-full overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column.key}
|
||||
className={cn(column.sortable && "cursor-pointer hover:bg-muted")}
|
||||
onClick={() => column.sortable && handleSort(column.key)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{column.header}
|
||||
{column.sortable && sortColumn === column.key && (
|
||||
<Icon
|
||||
name={sortDirection === 'asc' ? "ArrowUp" : "ArrowDown"}
|
||||
provider="lu"
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
))}
|
||||
{(onEdit || onDelete || onChangeOrder || customActions) && (
|
||||
<TableHead className="text-right">
|
||||
{t("common.actions")}
|
||||
</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item) => (
|
||||
<TableRow key={item[idField]}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={`${item[idField]}-${column.key}`}>
|
||||
{column.render ? column.render(item) : item[column.key] || "-"}
|
||||
</TableCell>
|
||||
))}
|
||||
{(onEdit || onDelete || onChangeOrder || customActions) && (
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{onEdit && (
|
||||
<EditAction
|
||||
item={item}
|
||||
idField={idField}
|
||||
editingItem={editingItem}
|
||||
setEditingItem={setEditingItem}
|
||||
onEdit={onEdit}
|
||||
FormComponent={FormComponent}
|
||||
editDialogTitle={editDialogTitle}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{showOrderButtons && onChangeOrder && orderingMode && (
|
||||
<OrderActions
|
||||
item={item}
|
||||
idField={idField}
|
||||
data={data}
|
||||
onChangeOrder={onChangeOrder}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<DeleteAction
|
||||
item={item}
|
||||
idField={idField}
|
||||
setItemToDelete={setItemToDelete}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{customActions && customActions(item)}
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
247
apps/web/src/components/admin/data-table/index.tsx
Normal file
247
apps/web/src/components/admin/data-table/index.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Icon } from '@/components/icon';
|
||||
import { MobileDataCard } from "./mobile-data-card";
|
||||
import { DesktopDataTable } from "./desktop-data-table";
|
||||
import { type ResponsiveDataTableProps } from "./types";
|
||||
|
||||
const ResponsiveDataTable = ({
|
||||
title,
|
||||
addButtonText,
|
||||
columns,
|
||||
data,
|
||||
FormComponent,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onChangeOrder,
|
||||
addDialogTitle,
|
||||
editDialogTitle,
|
||||
deleteDialogTitle,
|
||||
deleteDialogConfirmText,
|
||||
showOrderButtons = false,
|
||||
showAddButton = true,
|
||||
idField = "id",
|
||||
searchPlaceholder = "Search",
|
||||
customActions,
|
||||
}: ResponsiveDataTableProps) => {
|
||||
const t = useTranslations();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [orderingMode, setOrderingMode] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [itemToDelete, setItemToDelete] = useState<string | null>(null);
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
|
||||
// Filter data based on search term
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchTerm) return data;
|
||||
|
||||
return data.filter(item => {
|
||||
return columns.some(column => {
|
||||
if (!column.searchable) return false;
|
||||
|
||||
const value = item[column.key];
|
||||
if (!value) return false;
|
||||
|
||||
return value.toString().toLowerCase().includes(searchTerm.toLowerCase());
|
||||
});
|
||||
});
|
||||
}, [data, searchTerm, columns]);
|
||||
|
||||
// Sort data based on sort column and direction
|
||||
const filteredAndSortedData = useMemo(() => {
|
||||
if (!sortColumn) return filteredData;
|
||||
|
||||
return [...filteredData].sort((a, b) => {
|
||||
const aValue = a[sortColumn];
|
||||
const bValue = b[sortColumn];
|
||||
|
||||
if (aValue === bValue) return 0;
|
||||
|
||||
const comparison = aValue > bValue ? 1 : -1;
|
||||
return sortDirection === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}, [filteredData, sortColumn, sortDirection]);
|
||||
|
||||
// Handle delete
|
||||
const handleDelete = () => {
|
||||
if (itemToDelete && onDelete) {
|
||||
onDelete(itemToDelete);
|
||||
setItemToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mt-4">
|
||||
<CardHeader className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<div className="flex space-x-2 w-full sm:w-auto">
|
||||
{showAddButton && onAdd && (
|
||||
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full sm:w-auto">{addButtonText}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{addDialogTitle}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{FormComponent && (
|
||||
<FormComponent
|
||||
onSubmit={(data: any) => {
|
||||
if (onAdd) {
|
||||
onAdd(data);
|
||||
setAddDialogOpen(false);
|
||||
}
|
||||
}}
|
||||
onCancel={() => setAddDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-2 mb-4">
|
||||
<div className="max-w-sm">
|
||||
<Input
|
||||
placeholder={searchPlaceholder == "Search" ? t("common.search") : searchPlaceholder}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
{showOrderButtons && onChangeOrder && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="ordering-mode"
|
||||
checked={orderingMode}
|
||||
onCheckedChange={setOrderingMode}
|
||||
/>
|
||||
<Label htmlFor="ordering-mode">{t("common.changeOrder")}</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop Table View */}
|
||||
<div className="hidden lg:block">
|
||||
{filteredAndSortedData.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{searchTerm ? t("common.noResults") : t("common.noData")}
|
||||
</div>
|
||||
) : (
|
||||
<DesktopDataTable
|
||||
columns={columns}
|
||||
data={filteredAndSortedData}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
setSortColumn={setSortColumn}
|
||||
setSortDirection={setSortDirection}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onChangeOrder={onChangeOrder}
|
||||
editingItem={editingItem}
|
||||
setEditingItem={setEditingItem}
|
||||
setItemToDelete={setItemToDelete}
|
||||
FormComponent={FormComponent}
|
||||
editDialogTitle={editDialogTitle}
|
||||
showOrderButtons={showOrderButtons}
|
||||
orderingMode={orderingMode}
|
||||
idField={idField}
|
||||
t={t}
|
||||
customActions={customActions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<div className="block lg:hidden">
|
||||
{filteredAndSortedData.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{searchTerm ? t("common.noResults") : t("common.noData")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredAndSortedData.map((item) => (
|
||||
<MobileDataCard
|
||||
key={item[idField]}
|
||||
item={item}
|
||||
columns={columns}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onChangeOrder={onChangeOrder}
|
||||
editingItem={editingItem}
|
||||
setEditingItem={setEditingItem}
|
||||
setItemToDelete={setItemToDelete}
|
||||
FormComponent={FormComponent}
|
||||
editDialogTitle={editDialogTitle}
|
||||
deleteDialogTitle={deleteDialogTitle}
|
||||
deleteDialogConfirmText={deleteDialogConfirmText}
|
||||
showOrderButtons={showOrderButtons}
|
||||
orderingMode={orderingMode}
|
||||
data={data}
|
||||
idField={idField}
|
||||
t={t}
|
||||
customActions={customActions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Dialog */}
|
||||
{itemToDelete && (
|
||||
<Dialog
|
||||
open={!!itemToDelete}
|
||||
onOpenChange={(open) => !open && setItemToDelete(null)}
|
||||
>
|
||||
<DialogContent className="max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{deleteDialogTitle}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p>{deleteDialogConfirmText}</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setItemToDelete(null)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Re-export components for direct access
|
||||
export * from "./types";
|
||||
export * from "./mobile-data-card";
|
||||
export * from "./desktop-data-table";
|
||||
|
||||
// Export the main component
|
||||
export { ResponsiveDataTable };
|
||||
137
apps/web/src/components/admin/data-table/mobile-data-card.tsx
Normal file
137
apps/web/src/components/admin/data-table/mobile-data-card.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import type { MobileDataCardProps, Column } from "./types";
|
||||
import { EditAction, DeleteAction, OrderActions } from "./data-table-actions";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export const MobileDataCard = ({
|
||||
item,
|
||||
columns,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onChangeOrder,
|
||||
editingItem,
|
||||
setEditingItem,
|
||||
setItemToDelete,
|
||||
FormComponent,
|
||||
editDialogTitle,
|
||||
showOrderButtons,
|
||||
orderingMode,
|
||||
data,
|
||||
idField,
|
||||
t,
|
||||
customActions
|
||||
}: MobileDataCardProps) => {
|
||||
// Separate columns by priority
|
||||
const highPriorityColumns = columns.filter((col: Column) => col.priority === 'high');
|
||||
const mediumPriorityColumns = columns.filter((col: Column) => col.priority === 'medium');
|
||||
const lowPriorityColumns = columns.filter((col: Column) => col.priority === 'low' || !col.priority);
|
||||
|
||||
const renderColumnValue = (column: Column, item: any) => {
|
||||
const content = column.render ? column.render(item) : item[column.key] || "-";
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-2 overflow-hidden">
|
||||
<CardContent className="p-2 sm:p-4">
|
||||
{/* High priority fields - always visible, larger text */}
|
||||
{highPriorityColumns.length > 0 && (
|
||||
<div className="space-y-1 mb-2">
|
||||
{highPriorityColumns.map((column: Column) => (
|
||||
<div key={column.key}>
|
||||
<div className="font-medium text-base truncate w-full" title={typeof renderColumnValue(column, item) === 'string' ? renderColumnValue(column, item) : undefined}>
|
||||
{renderColumnValue(column, item)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Medium priority fields */}
|
||||
{mediumPriorityColumns.length > 0 && (
|
||||
<div className="space-y-1 mb-2">
|
||||
{mediumPriorityColumns.map((column: Column) => (
|
||||
<div key={column.key} className="flex flex-col sm:flex-row sm:justify-between sm:items-center text-sm">
|
||||
<span className="text-muted-foreground font-medium">{column.header}:</span>
|
||||
<span className="truncate w-full sm:max-w-[180px] sm:text-right" title={typeof renderColumnValue(column, item) === 'string' ? renderColumnValue(column, item) : undefined}>
|
||||
{renderColumnValue(column, item)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Low priority fields - collapsible */}
|
||||
{lowPriorityColumns.length > 0 && (
|
||||
<details className="mb-2">
|
||||
<summary className="text-sm text-muted-foreground cursor-pointer hover:text-foreground">
|
||||
{t("common.showMore")} ({lowPriorityColumns.length})
|
||||
</summary>
|
||||
<div className="mt-2 space-y-1">
|
||||
{lowPriorityColumns.map((column: Column) => (
|
||||
<div key={column.key} className="flex flex-col sm:flex-row sm:justify-between sm:items-center text-sm">
|
||||
<span className="text-muted-foreground font-medium">{column.header}:</span>
|
||||
<span className="truncate w-full sm:max-w-[180px] sm:text-right" title={typeof renderColumnValue(column, item) === 'string' ? renderColumnValue(column, item) : undefined}>
|
||||
{renderColumnValue(column, item)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{(onEdit || onDelete || onChangeOrder || customActions) && (
|
||||
<div className="flex flex-wrap gap-1 pt-2 border-t">
|
||||
{onDelete && (
|
||||
<DeleteAction
|
||||
item={item}
|
||||
idField={idField}
|
||||
setItemToDelete={setItemToDelete}
|
||||
isMobile={true}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{onEdit && (
|
||||
<EditAction
|
||||
item={item}
|
||||
idField={idField}
|
||||
editingItem={editingItem}
|
||||
setEditingItem={setEditingItem}
|
||||
onEdit={onEdit}
|
||||
FormComponent={FormComponent}
|
||||
editDialogTitle={editDialogTitle}
|
||||
isMobile={true}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showOrderButtons && onChangeOrder && orderingMode && (
|
||||
<OrderActions
|
||||
item={item}
|
||||
idField={idField}
|
||||
data={data}
|
||||
onChangeOrder={onChangeOrder}
|
||||
isMobile={true}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{customActions && (
|
||||
<div className="flex flex-wrap gap-1 w-full mt-1">
|
||||
{customActions(item)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
86
apps/web/src/components/admin/data-table/types.ts
Normal file
86
apps/web/src/components/admin/data-table/types.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type Column = {
|
||||
key: string;
|
||||
header: string;
|
||||
render?: (item: any) => ReactNode;
|
||||
sortable?: boolean;
|
||||
searchable?: boolean;
|
||||
priority?: 'high' | 'medium' | 'low';
|
||||
};
|
||||
|
||||
export interface ResponsiveDataTableProps {
|
||||
title: string;
|
||||
addButtonText: string;
|
||||
columns: Column[];
|
||||
data: any[];
|
||||
FormComponent?: React.ComponentType<{
|
||||
initialData?: any;
|
||||
onSubmit: (data: any) => void;
|
||||
onCancel: () => void;
|
||||
}>;
|
||||
onAdd?: (data: any) => void;
|
||||
onEdit?: (id: string, data: any) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
onChangeOrder?: (id: string, direction: "up" | "down") => void;
|
||||
addDialogTitle?: string;
|
||||
editDialogTitle?: string;
|
||||
deleteDialogTitle?: string;
|
||||
deleteDialogConfirmText?: string;
|
||||
showOrderButtons?: boolean;
|
||||
showAddButton?: boolean;
|
||||
idField?: string;
|
||||
searchPlaceholder?: string;
|
||||
customActions?: (item: any) => ReactNode;
|
||||
}
|
||||
|
||||
export interface MobileDataCardProps {
|
||||
item: any;
|
||||
columns: Column[];
|
||||
onEdit?: (id: string, data: any) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
onChangeOrder?: (id: string, direction: "up" | "down") => void;
|
||||
editingItem: any;
|
||||
setEditingItem: (item: any) => void;
|
||||
setItemToDelete: (id: string | null) => void;
|
||||
FormComponent?: React.ComponentType<{
|
||||
initialData?: any;
|
||||
onSubmit: (data: any) => void;
|
||||
onCancel: () => void;
|
||||
}>;
|
||||
editDialogTitle?: string;
|
||||
deleteDialogTitle?: string;
|
||||
deleteDialogConfirmText?: string;
|
||||
showOrderButtons?: boolean;
|
||||
orderingMode?: boolean;
|
||||
data: any[];
|
||||
idField: string;
|
||||
t: (key: string) => string;
|
||||
customActions?: (item: any) => ReactNode;
|
||||
}
|
||||
|
||||
export interface DesktopDataTableProps {
|
||||
columns: Column[];
|
||||
data: any[];
|
||||
sortColumn: string | null;
|
||||
sortDirection: 'asc' | 'desc';
|
||||
setSortColumn: (column: string | null) => void;
|
||||
setSortDirection: (direction: 'asc' | 'desc') => void;
|
||||
onEdit?: (id: string, data: any) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
onChangeOrder?: (id: string, direction: "up" | "down") => void;
|
||||
editingItem: any;
|
||||
setEditingItem: (item: any) => void;
|
||||
setItemToDelete: (id: string | null) => void;
|
||||
FormComponent?: React.ComponentType<{
|
||||
initialData?: any;
|
||||
onSubmit: (data: any) => void;
|
||||
onCancel: () => void;
|
||||
}>;
|
||||
editDialogTitle?: string;
|
||||
showOrderButtons?: boolean;
|
||||
orderingMode?: boolean;
|
||||
idField: string;
|
||||
t: (key: string) => string;
|
||||
customActions?: (item: any) => ReactNode;
|
||||
}
|
||||
@@ -62,9 +62,9 @@ export const EntityForm = ({
|
||||
|
||||
if (field.required) {
|
||||
if (field.type === "number") {
|
||||
validator = (validator as z.ZodNumber).min(0, t("validation.required") as string);
|
||||
validator = (validator as z.ZodNumber).min(0, t("components.validation.required") as string);
|
||||
} else if (field.type !== "switch") {
|
||||
validator = (validator as z.ZodString).min(1, t("validation.required") as string);
|
||||
validator = (validator as z.ZodString).min(1, t("components.validation.required") as string);
|
||||
}
|
||||
} else {
|
||||
if (field.type !== "switch") {
|
||||
|
||||
@@ -201,14 +201,14 @@ export default function FileUpload({
|
||||
{isPending ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Icon name="Loader" provider="lu" className="h-8 w-8 animate-spin" />
|
||||
<p className="text-sm text-muted-foreground">{t("fileUpload.uploading")}</p>
|
||||
<p className="text-sm text-muted-foreground">{t("components.fileUpload.uploading")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Icon name="Image" provider="lu" className="h-8 w-8 text-muted-foreground" />
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("fileUpload.dragAndDrop")}
|
||||
{t("components.fileUpload.dragAndDrop")}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -216,7 +216,7 @@ export default function FileUpload({
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Icon name="Upload" provider="lu" className="h-4 w-4 mr-2" />
|
||||
{t("fileUpload.chooseFile")}
|
||||
{t("components.fileUpload.chooseFile")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,7 +237,7 @@ export default function FileUpload({
|
||||
size="sm"
|
||||
onClick={handleClear}
|
||||
>
|
||||
{t("fileUpload.clear")}
|
||||
{t("components.fileUpload.clear")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -21,18 +21,18 @@ export function ModeToggle() {
|
||||
<Button variant="outline" size="icon">
|
||||
<Icon name="Sun" provider="lu" className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Icon name="Moon" provider="lu" className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">{t('theme.toggle')}</span>
|
||||
<span className="sr-only">{t('components.theme.toggle')}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
{t('theme.light')}
|
||||
{t('components.theme.light')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
{t('theme.dark')}
|
||||
{t('components.theme.dark')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
{t('theme.system')}
|
||||
{t('components.theme.system')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -55,8 +55,8 @@ export const NavigationForm = ({
|
||||
|
||||
const schema = z.object({
|
||||
label: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
url: z.string().nullable().optional(),
|
||||
external: z.boolean().default(false),
|
||||
|
||||
@@ -84,38 +84,38 @@ export default function NewsletterNotifications() {
|
||||
const selectedArticleData = articles?.find(post => post.id === selectedArticle);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 max-w-full overflow-hidden w-full">
|
||||
{/* Newsletter Stats */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Icon name="Users" provider="lu" className="h-5 w-5" />
|
||||
<Card className="w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl mx-auto md:mx-0">
|
||||
<CardHeader className="w-full">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Icon name="Users" provider="lu" className="h-4 w-4" />
|
||||
{t('newsletter.admin.stats.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs">
|
||||
{t('newsletter.admin.stats.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="w-full">
|
||||
{stats ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-1 w-full">
|
||||
<div className="text-center p-2 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||
<div className="text-xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{stats.polish.count}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{t('newsletter.admin.stats.polish')}</div>
|
||||
<div className="text-xs text-muted-foreground">{t('newsletter.admin.stats.polish')}</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
<div className="text-center p-2 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||
<div className="text-xl font-bold text-green-600 dark:text-green-400">
|
||||
{stats.english.count}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{t('newsletter.admin.stats.english')}</div>
|
||||
<div className="text-xs text-muted-foreground">{t('newsletter.admin.stats.english')}</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">
|
||||
<div className="text-center p-2 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||
<div className="text-xl font-bold text-purple-600 dark:text-purple-400">
|
||||
{stats.total.count}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{t('newsletter.admin.stats.total')}</div>
|
||||
<div className="text-xs text-muted-foreground">{t('newsletter.admin.stats.total')}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -128,27 +128,28 @@ export default function NewsletterNotifications() {
|
||||
</Card>
|
||||
|
||||
{/* Send Newsletter */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Icon name="Send" provider="lu" className="h-5 w-5" />
|
||||
<Card className="w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl mx-auto md:mx-0">
|
||||
<CardHeader className="w-full">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Icon name="Send" provider="lu" className="h-4 w-4" />
|
||||
{t('newsletter.admin.send.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs">
|
||||
{t('newsletter.admin.send.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<CardContent className="space-y-4 w-full">
|
||||
<div className="space-y-4 w-full">
|
||||
<Label htmlFor="article-select">{t('newsletter.admin.send.selectArticle')}</Label>
|
||||
<Select
|
||||
value={selectedArticle || ""}
|
||||
onValueChange={(value) => setSelectedArticle(value)}
|
||||
disabled={articlesLoading || isProcessing}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="max-w-full">
|
||||
<SelectValue placeholder={t('newsletter.admin.send.articlePlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent className="max-w-full">
|
||||
{articlesLoading ? (
|
||||
<div className="p-2 text-center">
|
||||
<Icon name="Loader" provider="lu" className="h-4 w-4 animate-spin mx-auto" />
|
||||
@@ -157,12 +158,12 @@ export default function NewsletterNotifications() {
|
||||
articles?.map((post) => {
|
||||
const title = typeof post.title === 'string' ? JSON.parse(post.title) : post.title;
|
||||
return (
|
||||
<SelectItem key={post.id} value={post.id}>
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium">
|
||||
<SelectItem key={post.id} value={post.id} className="max-w-full">
|
||||
<div className="flex flex-col items-start w-full max-w-full overflow-hidden">
|
||||
<span className="font-medium truncate w-full" title={title?.pl || title?.en || t('common.untitled')}>
|
||||
{title?.pl || title?.en || t('common.untitled')}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-muted-foreground truncate w-full">
|
||||
{post.publishedAt
|
||||
? new Date(post.publishedAt).toLocaleDateString()
|
||||
: t('common.draft')
|
||||
@@ -178,7 +179,7 @@ export default function NewsletterNotifications() {
|
||||
</div>
|
||||
|
||||
{selectedArticleData && (
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="p-4 bg-muted/50 rounded-lg overflow-hidden">
|
||||
<h4 className="font-medium mb-2">{t('newsletter.admin.send.preview')}</h4>
|
||||
<div className="space-y-1 text-sm">
|
||||
{(() => {
|
||||
@@ -187,10 +188,30 @@ export default function NewsletterNotifications() {
|
||||
: selectedArticleData.title;
|
||||
return (
|
||||
<>
|
||||
<p><strong>{t('newsletter.admin.send.polishTitle')}</strong> {title?.pl || t('newsletter.admin.send.notAvailable')}</p>
|
||||
<p><strong>{t('newsletter.admin.send.englishTitle')}</strong> {title?.en || t('newsletter.admin.send.notAvailable')}</p>
|
||||
<p><strong>{t('newsletter.admin.send.slug')}</strong> {selectedArticleData.slug}</p>
|
||||
<p><strong>{t('newsletter.admin.send.status')}</strong> {selectedArticleData.isPublished ? t('newsletter.admin.send.published') : t('newsletter.admin.send.draft')}</p>
|
||||
<div className="grid grid-cols-[auto,1fr] gap-x-2">
|
||||
<strong>{t('newsletter.admin.send.polishTitle')}</strong>
|
||||
<span className="truncate" title={title?.pl || t('newsletter.admin.send.notAvailable')}>
|
||||
{title?.pl || t('newsletter.admin.send.notAvailable')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[auto,1fr] gap-x-2">
|
||||
<strong>{t('newsletter.admin.send.englishTitle')}</strong>
|
||||
<span className="truncate" title={title?.en || t('newsletter.admin.send.notAvailable')}>
|
||||
{title?.en || t('newsletter.admin.send.notAvailable')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[auto,1fr] gap-x-2">
|
||||
<strong>{t('newsletter.admin.send.slug')}</strong>
|
||||
<span className="truncate" title={selectedArticleData.slug}>
|
||||
{selectedArticleData.slug}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[auto,1fr] gap-x-2">
|
||||
<strong>{t('newsletter.admin.send.status')}</strong>
|
||||
<span>
|
||||
{selectedArticleData.isPublished ? t('newsletter.admin.send.published') : t('newsletter.admin.send.draft')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
@@ -198,24 +219,26 @@ export default function NewsletterNotifications() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col gap-2 w-full min-w-0 overflow-hidden">
|
||||
<Button
|
||||
onClick={() => openConfirmModal('pl')}
|
||||
disabled={!selectedArticle || isProcessing}
|
||||
className="flex-1"
|
||||
className="w-full justify-start px-3"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="Mail" provider="lu" className="mr-2 h-4 w-4" />
|
||||
{t('newsletter.admin.send.sendToPolish')} ({getSubscriberCount('pl')})
|
||||
<Icon name="Mail" provider="lu" className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">{t('newsletter.admin.send.sendToPolish')} ({getSubscriberCount('pl')})</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => openConfirmModal('en')}
|
||||
disabled={!selectedArticle || isProcessing}
|
||||
className="flex-1"
|
||||
className="w-full justify-start px-3"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="Mail" provider="lu" className="mr-2 h-4 w-4" />
|
||||
{t('newsletter.admin.send.sendToEnglish')} ({getSubscriberCount('en')})
|
||||
<Icon name="Mail" provider="lu" className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">{t('newsletter.admin.send.sendToEnglish')} ({getSubscriberCount('en')})</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -223,7 +246,7 @@ export default function NewsletterNotifications() {
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
<Dialog open={isConfirmModalOpen} onOpenChange={setIsConfirmModalOpen}>
|
||||
<DialogContent>
|
||||
<DialogContent className="w-[95vw] max-w-[95vw] min-w-0 overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Icon name="AlertTriangle" provider="lu" className="h-5 w-5 text-yellow-500" />
|
||||
|
||||
@@ -70,12 +70,12 @@ export const ProjectForm = ({
|
||||
|
||||
const schema = z.object({
|
||||
title: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
description: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
url: z.string().nullable().optional(),
|
||||
repoUrl: z.string().nullable().optional(),
|
||||
|
||||
@@ -301,7 +301,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
isActive={editor.isActive('bold')}
|
||||
title={t('richEditor.toolbar.bold')}
|
||||
title={t('components.richEditor.toolbar.bold')}
|
||||
>
|
||||
<Icon name="Bold" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -309,7 +309,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
isActive={editor.isActive('italic')}
|
||||
title={t('richEditor.toolbar.italic')}
|
||||
title={t('components.richEditor.toolbar.italic')}
|
||||
>
|
||||
<Icon name="Italic" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -317,7 +317,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
isActive={editor.isActive('strike')}
|
||||
title={t('richEditor.toolbar.strikethrough')}
|
||||
title={t('components.richEditor.toolbar.strikethrough')}
|
||||
>
|
||||
<Icon name="Strikethrough" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -325,7 +325,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
isActive={editor.isActive('code')}
|
||||
title={t('richEditor.toolbar.code')}
|
||||
title={t('components.richEditor.toolbar.code')}
|
||||
>
|
||||
<Icon name="Code" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -335,7 +335,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
isActive={editor.isActive('heading', { level: 1 })}
|
||||
title={t('richEditor.toolbar.heading1')}
|
||||
title={t('components.richEditor.toolbar.heading1')}
|
||||
>
|
||||
<Icon name="Heading1" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -343,7 +343,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
isActive={editor.isActive('heading', { level: 2 })}
|
||||
title={t('richEditor.toolbar.heading2')}
|
||||
title={t('components.richEditor.toolbar.heading2')}
|
||||
>
|
||||
<Icon name="Heading2" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -351,7 +351,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
isActive={editor.isActive('heading', { level: 3 })}
|
||||
title={t('richEditor.toolbar.heading3')}
|
||||
title={t('components.richEditor.toolbar.heading3')}
|
||||
>
|
||||
<Icon name="Heading3" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -361,7 +361,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
isActive={editor.isActive('bulletList')}
|
||||
title={t('richEditor.toolbar.bulletList')}
|
||||
title={t('components.richEditor.toolbar.bulletList')}
|
||||
>
|
||||
<Icon name="List" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -369,7 +369,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
isActive={editor.isActive('orderedList')}
|
||||
title={t('richEditor.toolbar.orderedList')}
|
||||
title={t('components.richEditor.toolbar.orderedList')}
|
||||
>
|
||||
<Icon name="ListOrdered" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -377,7 +377,7 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
isActive={editor.isActive('blockquote')}
|
||||
title={t('richEditor.toolbar.quote')}
|
||||
title={t('components.richEditor.toolbar.quote')}
|
||||
>
|
||||
<Icon name="Quote" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -387,21 +387,21 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<ToolbarButton
|
||||
onClick={setLink}
|
||||
isActive={editor.isActive('link')}
|
||||
title={t('richEditor.toolbar.addLink')}
|
||||
title={t('components.richEditor.toolbar.addLink')}
|
||||
>
|
||||
<Icon name="Link" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarButton
|
||||
onClick={addImage}
|
||||
title={t('richEditor.toolbar.addImage')}
|
||||
title={t('components.richEditor.toolbar.addImage')}
|
||||
>
|
||||
<Icon name="Image" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarButton
|
||||
onClick={addYouTube}
|
||||
title={t('richEditor.toolbar.addYoutube')}
|
||||
title={t('components.richEditor.toolbar.addYoutube')}
|
||||
>
|
||||
<Icon name="Youtube" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -410,21 +410,21 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
|
||||
<ToolbarButton
|
||||
onClick={addCodeBlock}
|
||||
title={t('richEditor.toolbar.addCodeBlock')}
|
||||
title={t('components.richEditor.toolbar.addCodeBlock')}
|
||||
>
|
||||
<Icon name="CodeBlock" provider="lu" className="h-4 w-4" />
|
||||
<Icon name="SquareCode" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
title={t('richEditor.toolbar.undo')}
|
||||
title={t('components.richEditor.toolbar.undo')}
|
||||
>
|
||||
<Icon name="Undo" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
title={t('richEditor.toolbar.redo')}
|
||||
title={t('components.richEditor.toolbar.redo')}
|
||||
>
|
||||
<Icon name="Redo" provider="lu" className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
@@ -442,19 +442,19 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.addImage')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('richEditor.modals.image.description')}
|
||||
{t('components.richEditor.modals.image.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<FileUpload
|
||||
category="blog"
|
||||
onChange={handleImageUpload}
|
||||
label={t('fileUpload.selectOrUploadImage')}
|
||||
label={t('components.fileUpload.selectOrUploadImage')}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowImageUpload(false)}>
|
||||
{t('richEditor.modals.image.cancel')}
|
||||
{t('components.richEditor.modals.image.cancel')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -464,28 +464,28 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<Dialog open={showLinkModal} onOpenChange={setShowLinkModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('richEditor.modals.link.title')}</DialogTitle>
|
||||
<DialogTitle>{t('components.richEditor.modals.link.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('richEditor.modals.link.description')}
|
||||
{t('components.richEditor.modals.link.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="link-url">{t('richEditor.modals.link.urlLabel')}</Label>
|
||||
<Label htmlFor="link-url">{t('components.richEditor.modals.link.urlLabel')}</Label>
|
||||
<Input
|
||||
id="link-url"
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
placeholder={t('richEditor.modals.link.urlPlaceholder')}
|
||||
placeholder={t('components.richEditor.modals.link.urlPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowLinkModal(false)}>
|
||||
{t('richEditor.modals.link.cancel')}
|
||||
{t('components.richEditor.modals.link.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleLinkSubmit}>
|
||||
{linkUrl ? t('richEditor.modals.link.addLink') : t('richEditor.modals.link.removeLink')}
|
||||
{linkUrl ? t('components.richEditor.modals.link.addLink') : t('components.richEditor.modals.link.removeLink')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -495,44 +495,44 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<Dialog open={showYoutubeModal} onOpenChange={setShowYoutubeModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('richEditor.modals.youtube.title')}</DialogTitle>
|
||||
<DialogTitle>{t('components.richEditor.modals.youtube.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('richEditor.modals.youtube.description')}
|
||||
{t('components.richEditor.modals.youtube.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="youtube-url">{t('richEditor.modals.youtube.urlLabel')}</Label>
|
||||
<Label htmlFor="youtube-url">{t('components.richEditor.modals.youtube.urlLabel')}</Label>
|
||||
<Input
|
||||
id="youtube-url"
|
||||
value={youtubeUrl}
|
||||
onChange={(e) => setYoutubeUrl(e.target.value)}
|
||||
placeholder={t('richEditor.modals.youtube.urlPlaceholder')}
|
||||
placeholder={t('components.richEditor.modals.youtube.urlPlaceholder')}
|
||||
/>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="font-medium mb-1">{t('richEditor.modals.youtube.supportedFormats')}</p>
|
||||
<p className="font-medium mb-1">{t('components.richEditor.modals.youtube.supportedFormats')}</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{t.raw('richEditor.modals.youtube.formats').map((format: string, index: number) => (
|
||||
{t.raw('components.richEditor.modals.youtube.formats').map((format: string, index: number) => (
|
||||
<li key={index}>{format}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{youtubeUrl && !/^(https?:\/\/)?(www\.)?(youtube\.com\/(watch\?v=|embed\/)|youtu\.be\/)[\w-]+/.test(youtubeUrl) && (
|
||||
<p className="text-sm text-destructive">
|
||||
{t('richEditor.modals.youtube.invalidUrl')}
|
||||
{t('components.richEditor.modals.youtube.invalidUrl')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowYoutubeModal(false)}>
|
||||
{t('richEditor.modals.youtube.cancel')}
|
||||
{t('components.richEditor.modals.youtube.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleYoutubeSubmit}
|
||||
disabled={!youtubeUrl || !/^(https?:\/\/)?(www\.)?(youtube\.com\/(watch\?v=|embed\/)|youtu\.be\/)[\w-]+/.test(youtubeUrl)}
|
||||
>
|
||||
{t('richEditor.modals.youtube.addVideo')}
|
||||
{t('components.richEditor.modals.youtube.addVideo')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -542,33 +542,33 @@ export default function RichTextEditor({ content, onChange, placeholder }: RichT
|
||||
<Dialog open={showCodeBlockModal} onOpenChange={setShowCodeBlockModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('richEditor.modals.codeBlock.title')}</DialogTitle>
|
||||
<DialogTitle>{t('components.richEditor.modals.codeBlock.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('richEditor.modals.codeBlock.description')}
|
||||
{t('components.richEditor.modals.codeBlock.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="code-language">{t('richEditor.modals.codeBlock.languageLabel')}</Label>
|
||||
<Label htmlFor="code-language">{t('components.richEditor.modals.codeBlock.languageLabel')}</Label>
|
||||
<Input
|
||||
id="code-language"
|
||||
value={codeLanguage}
|
||||
onChange={(e) => setCodeLanguage(e.target.value)}
|
||||
placeholder={t('richEditor.modals.codeBlock.languagePlaceholder')}
|
||||
placeholder={t('components.richEditor.modals.codeBlock.languagePlaceholder')}
|
||||
/>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="font-medium mb-1">{t('richEditor.modals.codeBlock.popularLanguages')}</p>
|
||||
<p>{t('richEditor.modals.codeBlock.languagesList')}</p>
|
||||
<p className="mt-2">{t('richEditor.modals.codeBlock.noHighlighting')}</p>
|
||||
<p className="font-medium mb-1">{t('components.richEditor.modals.codeBlock.popularLanguages')}</p>
|
||||
<p>{t('components.richEditor.modals.codeBlock.languagesList')}</p>
|
||||
<p className="mt-2">{t('components.richEditor.modals.codeBlock.noHighlighting')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCodeBlockModal(false)}>
|
||||
{t('richEditor.modals.codeBlock.cancel')}
|
||||
{t('components.richEditor.modals.codeBlock.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleCodeBlockSubmit}>
|
||||
{t('richEditor.modals.codeBlock.addCodeBlock')}
|
||||
{t('components.richEditor.modals.codeBlock.addCodeBlock')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -32,8 +32,8 @@ export const SkillCategoryForm = ({
|
||||
|
||||
const schema = z.object({
|
||||
name: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ export const SkillForm = ({
|
||||
|
||||
const schema = z.object({
|
||||
name: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
categoryId: z.string().nullable(),
|
||||
iconName: z.string().optional().nullable(),
|
||||
|
||||
@@ -64,8 +64,8 @@ export const TopBarForm = ({
|
||||
|
||||
const schema = z.object({
|
||||
name: z.object({
|
||||
pl: z.string().min(1, t("validation.required") as string),
|
||||
en: z.string().min(1, t("validation.required") as string),
|
||||
pl: z.string().min(1, t("components.validation.required") as string),
|
||||
en: z.string().min(1, t("components.validation.required") as string),
|
||||
}),
|
||||
iconName: z.string().nullable().optional(),
|
||||
iconProvider: z.string().nullable().optional(),
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function UserMenu() {
|
||||
if (!session) {
|
||||
return (
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/login">{t('userMenu.signIn')}</Link>
|
||||
<Link href="/login">{t('components.userMenu.signIn')}</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export default function UserMenu() {
|
||||
<Button variant="outline">{session.user.name}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-card">
|
||||
<DropdownMenuLabel>{t('userMenu.myAccount')}</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t('components.userMenu.myAccount')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
@@ -52,7 +52,7 @@ export default function UserMenu() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('userMenu.signOut')}
|
||||
{t('components.userMenu.signOut')}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -67,27 +67,27 @@ export default function CommentsSection({ postId }: CommentsSectionProps) {
|
||||
const newErrors: Partial<CommentFormData> = {};
|
||||
|
||||
if (!formData.authorName.trim()) {
|
||||
newErrors.authorName = t('validation.required');
|
||||
newErrors.authorName = t('components.validation.required');
|
||||
}
|
||||
|
||||
if (!formData.authorEmail.trim()) {
|
||||
newErrors.authorEmail = t('validation.required');
|
||||
newErrors.authorEmail = t('components.validation.required');
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.authorEmail)) {
|
||||
newErrors.authorEmail = t('validation.invalid');
|
||||
newErrors.authorEmail = t('components.validation.invalid');
|
||||
}
|
||||
|
||||
if (formData.authorWebsite && !/^https?:\/\/.+/.test(formData.authorWebsite)) {
|
||||
newErrors.authorWebsite = t('validation.invalidUrl');
|
||||
newErrors.authorWebsite = t('components.validation.invalidUrl');
|
||||
}
|
||||
|
||||
if (!formData.content.trim()) {
|
||||
newErrors.content = t('validation.required');
|
||||
newErrors.content = t('components.validation.required');
|
||||
} else if (formData.content.trim().length < 10) {
|
||||
newErrors.content = t('validation.minLength', { min: 10 });
|
||||
newErrors.content = t('components.validation.minLength', { min: 10 });
|
||||
}
|
||||
|
||||
if (!formData.turnstileToken) {
|
||||
newErrors.turnstileToken = t('validation.turnstileRequired');
|
||||
newErrors.turnstileToken = t('components.validation.turnstileRequired');
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
@@ -43,13 +43,13 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
const sendEmailMutation = useMutation(
|
||||
trpc.mail.sendContactForm.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success(t('contact.form.success'));
|
||||
toast.success(t('pages.contact.form.success'));
|
||||
setFormData({ name: '', email: '', message: '', turnstileToken: '' });
|
||||
setErrors({});
|
||||
turnstileRef.current?.reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(t('contact.form.error'));
|
||||
toast.error(t('pages.contact.form.error'));
|
||||
console.error('Error sending email:', error);
|
||||
}
|
||||
})
|
||||
@@ -59,23 +59,23 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
const newErrors: Partial<FormData> = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = t('validation.required');
|
||||
newErrors.name = t('components.validation.required');
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = t('validation.required');
|
||||
newErrors.email = t('components.validation.required');
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = t('validation.invalid');
|
||||
newErrors.email = t('components.validation.invalid');
|
||||
}
|
||||
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = t('validation.required');
|
||||
newErrors.message = t('components.validation.required');
|
||||
} else if (formData.message.trim().length < 10) {
|
||||
newErrors.message = t('validation.minLength', { min: 10 });
|
||||
newErrors.message = t('components.validation.minLength', { min: 10 });
|
||||
}
|
||||
|
||||
if (!formData.turnstileToken) {
|
||||
newErrors.turnstileToken = t('validation.turnstileRequired');
|
||||
newErrors.turnstileToken = t('components.validation.turnstileRequired');
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
@@ -112,7 +112,7 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
<MainLayout>
|
||||
<div className="max-w-screen-lg mx-auto px-6 md:px-24 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6 text-gray-800 dark:text-gray-100">
|
||||
{t('contact.title')}
|
||||
{t('pages.contact.title')}
|
||||
</h1>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -125,7 +125,7 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
<Card className="border border-gray-200 dark:border-gray-800">
|
||||
<CardContent className="p-6">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-100">
|
||||
{t('contact.directContact')}
|
||||
{t('pages.contact.directContact')}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -175,16 +175,16 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
<Card className="border border-gray-200 dark:border-gray-800">
|
||||
<CardContent className="p-6">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-100">
|
||||
{t('contact.quickMessage')}
|
||||
{t('pages.contact.quickMessage')}
|
||||
</h2>
|
||||
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="name">{t('contact.form.name')}</Label>
|
||||
<Label htmlFor="name">{t('pages.contact.form.name')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
placeholder={t('contact.form.name')}
|
||||
placeholder={t('pages.contact.form.name')}
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className={errors.name ? 'border-rose-500' : ''}
|
||||
@@ -195,11 +195,11 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="email">{t('contact.form.email')}</Label>
|
||||
<Label htmlFor="email">{t('pages.contact.form.email')}</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
placeholder={t('contact.form.email')}
|
||||
placeholder={t('pages.contact.form.email')}
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className={errors.email ? 'border-rose-500' : ''}
|
||||
@@ -210,10 +210,10 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="message">{t('contact.form.message')}</Label>
|
||||
<Label htmlFor="message">{t('pages.contact.form.message')}</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder={t('contact.form.message')}
|
||||
placeholder={t('pages.contact.form.message')}
|
||||
className={`min-h-[120px] ${errors.message ? 'border-rose-500' : ''}`}
|
||||
value={formData.message}
|
||||
onChange={(e) => handleInputChange('message', e.target.value)}
|
||||
@@ -240,7 +240,7 @@ export default function ContactClientWrapper({ locale }: Props) {
|
||||
className="w-full bg-rose-600 hover:bg-rose-700 text-white"
|
||||
disabled={sendEmailMutation.isPending}
|
||||
>
|
||||
{sendEmailMutation.isPending ? t('common.saving') : t('contact.form.send')}
|
||||
{sendEmailMutation.isPending ? t('common.saving') : t('pages.contact.form.send')}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function Footer() {
|
||||
<div className="flex items-center gap-6 flex-wrap justify-center">
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">{t('theme.toggle')}</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">{t('components.theme.toggle')}</span>
|
||||
<Tabs
|
||||
id="theme-tabs"
|
||||
defaultValue={theme}
|
||||
@@ -47,7 +47,7 @@ export default function Footer() {
|
||||
aria-controls="theme-light-content"
|
||||
tabIndex={theme === 'light' ? 0 : -1}>
|
||||
<Icon name="Sun" provider="lu" />
|
||||
<span>{t('theme.light')}</span>
|
||||
<span>{t('components.theme.light')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="dark"
|
||||
@@ -56,7 +56,7 @@ export default function Footer() {
|
||||
aria-controls="theme-dark-content"
|
||||
tabIndex={theme === 'dark' ? 0 : -1}>
|
||||
<Icon name="Moon" provider="lu" />
|
||||
<span>{t('theme.dark')}</span>
|
||||
<span>{t('components.theme.dark')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="light" id="theme-light-content" className="hidden" />
|
||||
@@ -65,7 +65,7 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">{t('language.switch')}</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">{t('components.language.switch')}</span>
|
||||
<Tabs
|
||||
id="language-tabs"
|
||||
defaultValue={locale}
|
||||
@@ -110,7 +110,7 @@ export default function Footer() {
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
© {currentYear} {t('footer.copyright')}
|
||||
© {currentYear} {t('components.footer.copyright')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function NavigationBar() {
|
||||
})}
|
||||
</ul>
|
||||
<div className="hidden md:flex text-sm text-gray-900 dark:text-gray-100 mt-4 text-center items-center justify-center">
|
||||
<Icon name="MoveLeft" provider="lu" className="inline mr-1" />{t('navigation.keyboardHint')}<Icon name="MoveRight" provider="lu" className="inline ml-1" />
|
||||
<Icon name="MoveLeft" provider="lu" className="inline mr-1" />{t('components.navigation.keyboardHint')}<Icon name="MoveRight" provider="lu" className="inline ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function NewsletterSignup({
|
||||
e.preventDefault();
|
||||
|
||||
if (!email.trim()) {
|
||||
toast.error(t('validation.required'));
|
||||
toast.error(t('components.validation.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function ProjectsClientWrapper({ locale }: Props) {
|
||||
<MainLayout>
|
||||
<div className="max-w-screen-lg mx-auto px-6 md:px-24 py-8">
|
||||
<h1 className="text-3xl font-bold mb-4 text-gray-800 dark:text-gray-100">
|
||||
{t('projects.title')}
|
||||
{t('pages.projects.title')}
|
||||
</h1>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -220,7 +220,7 @@ export default function ProjectsClientWrapper({ locale }: Props) {
|
||||
onClick={() => project.repoUrl && window.open(project.repoUrl, '_blank', 'noopener,noreferrer')}
|
||||
>
|
||||
<Icon name="github" provider="si" className="text-rose-600 dark:text-rose-400" />
|
||||
{t('projects.github')}
|
||||
{t('pages.projects.github')}
|
||||
</Button>
|
||||
)}
|
||||
{project.repoUrl2 && (
|
||||
@@ -231,7 +231,7 @@ export default function ProjectsClientWrapper({ locale }: Props) {
|
||||
onClick={() => project.repoUrl2 && window.open(project.repoUrl2, '_blank', 'noopener,noreferrer')}
|
||||
>
|
||||
<Icon name="gitea" provider="si" className="text-rose-600 dark:text-rose-400" />
|
||||
{t('projects.gitea')}
|
||||
{t('pages.projects.gitea')}
|
||||
</Button>
|
||||
)}
|
||||
{project.url && (
|
||||
@@ -242,7 +242,7 @@ export default function ProjectsClientWrapper({ locale }: Props) {
|
||||
onClick={() => project.url && window.open(project.url, '_blank', 'noopener,noreferrer')}
|
||||
>
|
||||
<Icon name="ExternalLink" provider="lu" className="text-rose-600 dark:text-rose-400" />
|
||||
{t('projects.demo')}
|
||||
{t('pages.projects.demo')}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
@@ -251,7 +251,7 @@ export default function ProjectsClientWrapper({ locale }: Props) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-gray-600 dark:text-gray-400">
|
||||
{t('projects.noProjects')}
|
||||
{t('pages.projects.noProjects')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function SignInForm({
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
if (!turnstileToken) {
|
||||
setTurnstileError(t('validation.turnstileRequired'));
|
||||
setTurnstileError(t('components.validation.turnstileRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function SignInForm({
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.push("/admin")
|
||||
toast.success(t('login.success'));
|
||||
toast.success(t('pages.login.success'));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.error.message);
|
||||
@@ -69,7 +69,7 @@ export default function SignInForm({
|
||||
|
||||
const handleTurnstileError = () => {
|
||||
setTurnstileToken("");
|
||||
setTurnstileError(t('validation.turnstileRequired'));
|
||||
setTurnstileError(t('components.validation.turnstileRequired'));
|
||||
};
|
||||
|
||||
if (isPending) {
|
||||
@@ -78,7 +78,7 @@ export default function SignInForm({
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">{t('login.title')}</h1>
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">{t('pages.login.title')}</h1>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -92,7 +92,7 @@ export default function SignInForm({
|
||||
<form.Field name="email">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>{t('login.email')}</Label>
|
||||
<Label htmlFor={field.name}>{t('pages.login.email')}</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
@@ -115,7 +115,7 @@ export default function SignInForm({
|
||||
<form.Field name="password">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>{t('login.password')}</Label>
|
||||
<Label htmlFor={field.name}>{t('pages.login.password')}</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
@@ -153,7 +153,7 @@ export default function SignInForm({
|
||||
className="w-full"
|
||||
disabled={!state.canSubmit || state.isSubmitting}
|
||||
>
|
||||
{state.isSubmitting ? "Submitting..." : t('login.submit')}
|
||||
{state.isSubmitting ? t('common.submitting') : t('pages.login.submit')}
|
||||
</Button>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
@@ -161,13 +161,13 @@ export default function SignInForm({
|
||||
|
||||
{!disabledRegistration && (
|
||||
<div className="mt-4 text-center">
|
||||
<p className="text-sm text-gray-600">{t('login.noAccount')}</p>
|
||||
<p className="text-sm text-gray-600">{t('pages.login.noAccount')}</p>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={onSwitchToSignUp}
|
||||
className="text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
{t('login.register')}
|
||||
{t('pages.login.register')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function SignUpForm({
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
if (!turnstileToken) {
|
||||
setTurnstileError(t('validation.turnstileRequired'));
|
||||
setTurnstileError(t('components.validation.turnstileRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function SignUpForm({
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.push("/admin");
|
||||
toast.success(t('register.success'));
|
||||
toast.success(t('pages.register.success'));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.error.message);
|
||||
@@ -70,7 +70,7 @@ export default function SignUpForm({
|
||||
|
||||
const handleTurnstileError = () => {
|
||||
setTurnstileToken("");
|
||||
setTurnstileError(t('validation.turnstileRequired'));
|
||||
setTurnstileError(t('components.validation.turnstileRequired'));
|
||||
};
|
||||
|
||||
if (isPending) {
|
||||
@@ -79,7 +79,7 @@ export default function SignUpForm({
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">{t('register.title')}</h1>
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">{t('pages.register.title')}</h1>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -93,7 +93,7 @@ export default function SignUpForm({
|
||||
<form.Field name="name">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>{t('register.name')}</Label>
|
||||
<Label htmlFor={field.name}>{t('pages.register.name')}</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
@@ -115,7 +115,7 @@ export default function SignUpForm({
|
||||
<form.Field name="email">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>{t('login.email')}</Label>
|
||||
<Label htmlFor={field.name}>{t('pages.login.email')}</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
@@ -138,7 +138,7 @@ export default function SignUpForm({
|
||||
<form.Field name="password">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>{t('login.password')}</Label>
|
||||
<Label htmlFor={field.name}>{t('pages.login.password')}</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
@@ -176,20 +176,20 @@ export default function SignUpForm({
|
||||
className="w-full"
|
||||
disabled={!state.canSubmit || state.isSubmitting}
|
||||
>
|
||||
{state.isSubmitting ? "Submitting..." : t('register.submit')}
|
||||
{state.isSubmitting ? t('common.submitting') : t('pages.register.submit')}
|
||||
</Button>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<p className="text-sm text-gray-600">{t('register.haveAccount')}</p>
|
||||
<p className="text-sm text-gray-600">{t('pages.register.haveAccount')}</p>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={onSwitchToSignIn}
|
||||
className="text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
{t('register.signIn')}
|
||||
{t('pages.register.signIn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function SkillsClientWrapper({ locale }: Props) {
|
||||
<MainLayout>
|
||||
<div className="max-w-screen-lg mx-auto px-6 md:px-24 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6 text-gray-800 dark:text-gray-100">
|
||||
{t('skills.title')}
|
||||
{t('pages.skills.title')}
|
||||
</h1>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -81,7 +81,7 @@ export default function SkillsClientWrapper({ locale }: Props) {
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-4 text-gray-600 dark:text-gray-400">
|
||||
{t('skills.noSkills')}
|
||||
{t('pages.skills.noSkills')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ export default function SkillsClientWrapper({ locale }: Props) {
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-600 dark:text-gray-400">
|
||||
{t('skills.noCategories')}
|
||||
{t('pages.skills.noCategories')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
65
apps/web/src/components/ui/accordion.tsx
Normal file
65
apps/web/src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { Icon } from '@/components/icon';
|
||||
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Icon name="ChevronDown" provider="lu" className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -1,16 +1,43 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
import {hasLocale} from 'next-intl';
|
||||
import {routing} from './routing';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function loadMessagesFromDirectory(locale: string) {
|
||||
let messages: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
const messagesDirectory = path.join(process.cwd(), 'messages', locale);
|
||||
|
||||
if (fs.existsSync(messagesDirectory)) {
|
||||
const files = fs.readdirSync(messagesDirectory)
|
||||
.filter(file => file.endsWith('.json'));
|
||||
|
||||
for (const file of files) {
|
||||
const moduleName = file.replace('.json', '');
|
||||
const moduleMessages = (await import(`../../messages/${locale}/${file}`)).default;
|
||||
messages[moduleName] = moduleMessages;
|
||||
}
|
||||
} else {
|
||||
messages = (await import(`../../messages/${locale}.json`)).default;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load messages for ${locale}`, error);
|
||||
messages = (await import(`../../messages/${locale}.json`)).default;
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
export default getRequestConfig(async ({requestLocale}) => {
|
||||
// Typically corresponds to the `[locale]` segment
|
||||
const requested = await requestLocale;
|
||||
const locale = hasLocale(routing.locales, requested)
|
||||
? requested
|
||||
: routing.defaultLocale;
|
||||
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default
|
||||
messages: await loadMessagesFromDirectory(locale)
|
||||
};
|
||||
});
|
||||
3
bun.lock
3
bun.lock
@@ -40,6 +40,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.0",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
@@ -248,6 +249,8 @@
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, ""],
|
||||
|
||||
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""],
|
||||
|
||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""],
|
||||
|
||||
Reference in New Issue
Block a user