UI to share user to other users
This commit is contained in:
@@ -16,6 +16,11 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
charGrp.PUT("/:id/image", UpdateCharacterImage)
|
charGrp.PUT("/:id/image", UpdateCharacterImage)
|
||||||
charGrp.GET("/:id/datasheet-options", GetDatasheetOptions)
|
charGrp.GET("/:id/datasheet-options", GetDatasheetOptions)
|
||||||
|
|
||||||
|
// Character Sharing
|
||||||
|
charGrp.GET("/:id/shares", GetCharacterShares)
|
||||||
|
charGrp.PUT("/:id/shares", UpdateCharacterShares)
|
||||||
|
charGrp.GET("/:id/available-users", GetAvailableUsersForSharing)
|
||||||
|
|
||||||
// Erfahrung und Vermögen
|
// Erfahrung und Vermögen
|
||||||
charGrp.GET("/:id/experience-wealth", GetCharacterExperienceAndWealth) // NewSystem
|
charGrp.GET("/:id/experience-wealth", GetCharacterExperienceAndWealth) // NewSystem
|
||||||
charGrp.PUT("/:id/experience", UpdateCharacterExperience) // NewSystem
|
charGrp.PUT("/:id/experience", UpdateCharacterExperience) // NewSystem
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package character
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bamort/database"
|
||||||
|
"bamort/models"
|
||||||
|
"bamort/user"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCharacterShares returns the list of users a character is shared with
|
||||||
|
func GetCharacterShares(c *gin.Context) {
|
||||||
|
charID := c.Param("id")
|
||||||
|
|
||||||
|
var character models.Char
|
||||||
|
if err := character.FirstID(charID); err != nil {
|
||||||
|
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var shares []models.CharShare
|
||||||
|
if err := database.DB.Where("character_id = ?", character.ID).Find(&shares).Error; err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve shares")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user details for each share
|
||||||
|
type ShareWithUser struct {
|
||||||
|
models.CharShare
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var sharesWithUsers []ShareWithUser
|
||||||
|
for _, share := range shares {
|
||||||
|
var u user.User
|
||||||
|
if err := u.FirstId(share.UserID); err == nil {
|
||||||
|
sharesWithUsers = append(sharesWithUsers, ShareWithUser{
|
||||||
|
CharShare: share,
|
||||||
|
Username: u.Username,
|
||||||
|
Email: u.Email,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, sharesWithUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCharacterShares updates the list of users a character is shared with
|
||||||
|
func UpdateCharacterShares(c *gin.Context) {
|
||||||
|
charID := c.Param("id")
|
||||||
|
|
||||||
|
var character models.Char
|
||||||
|
if err := character.FirstID(charID); err != nil {
|
||||||
|
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSharesRequest struct {
|
||||||
|
UserIDs []uint `json:"user_ids" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var request UpdateSharesRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing shares
|
||||||
|
if err := database.DB.Where("character_id = ?", character.ID).Delete(&models.CharShare{}).Error; err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to delete existing shares")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new shares
|
||||||
|
for _, userID := range request.UserIDs {
|
||||||
|
// Don't share with yourself
|
||||||
|
if userID == character.UserID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
share := models.CharShare{
|
||||||
|
CharacterID: character.ID,
|
||||||
|
UserID: userID,
|
||||||
|
Permission: "read",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(&share).Error; err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to create share")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Shares updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAvailableUsersForSharing returns a list of users (excluding the owner)
|
||||||
|
func GetAvailableUsersForSharing(c *gin.Context) {
|
||||||
|
charID := c.Param("id")
|
||||||
|
|
||||||
|
var character models.Char
|
||||||
|
if err := character.FirstID(charID); err != nil {
|
||||||
|
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []user.User
|
||||||
|
if err := database.DB.Where("user_id != ?", character.UserID).Find(&users).Error; err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve users")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sensitive data
|
||||||
|
type UserInfo struct {
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfos []UserInfo
|
||||||
|
for _, u := range users {
|
||||||
|
userInfos = append(userInfos, UserInfo{
|
||||||
|
UserID: u.UserID,
|
||||||
|
Username: u.Username,
|
||||||
|
Email: u.Email,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, userInfos)
|
||||||
|
}
|
||||||
@@ -2544,7 +2544,7 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remove-btn {
|
.remove-btn {
|
||||||
position: absolute;
|
/*position: absolute;*/
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showDialog" class="modal-overlay" @click.self="closeDialog">
|
<div v-if="showDialog" class="modal-overlay" @click.self="closeDialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content modal-large">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>{{ $t('visibility.title') }}</h3>
|
<h3>{{ $t('visibility.title') }}</h3>
|
||||||
<button @click="closeDialog" class="close-button">×</button>
|
<button @click="closeDialog" class="close-button">×</button>
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<p>{{ $t('visibility.updating') }}</p>
|
<p>{{ $t('visibility.updating') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Visibility Options -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p>{{ $t('visibility.description') }}</p>
|
<p>{{ $t('visibility.description') }}</p>
|
||||||
<div class="visibility-options">
|
<div class="visibility-options">
|
||||||
@@ -39,12 +41,80 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Share with Specific Users Section -->
|
||||||
|
<div class="form-group">
|
||||||
|
<h4>{{ $t('visibility.shareWithUsers') }}</h4>
|
||||||
|
<p class="section-description">{{ $t('visibility.shareDescription') }}</p>
|
||||||
|
|
||||||
|
<div class="share-sections-container">
|
||||||
|
|
||||||
|
<!-- Add Users Section -->
|
||||||
|
<div class="add-users-section">
|
||||||
|
<h5>{{ $t('visibility.addUsers') }}</h5>
|
||||||
|
<div class="user-search">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('visibility.searchUsers')"
|
||||||
|
class="form-control"
|
||||||
|
:disabled="isUpdating"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoadingUsers" class="loading">{{ $t('visibility.loadingUsers') }}</div>
|
||||||
|
|
||||||
|
<div v-else-if="filteredAvailableUsers.length > 0" class="available-users-list">
|
||||||
|
<div
|
||||||
|
v-for="user in filteredAvailableUsers"
|
||||||
|
:key="user.user_id"
|
||||||
|
class="user-item"
|
||||||
|
@click="toggleUser(user.user_id)"
|
||||||
|
>
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">{{ user.username }}</span>
|
||||||
|
<span class="user-email">{{ user.email }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!isLoadingUsers && availableUsers.length === 0" class="no-users">
|
||||||
|
{{ $t('visibility.noOtherUsers') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="no-users">
|
||||||
|
{{ $t('visibility.noMatchingUsers') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Currently Shared Users -->
|
||||||
|
<div class="shared-users-list">
|
||||||
|
<h5>{{ $t('visibility.currentlySharedWith') }}</h5>
|
||||||
|
<div v-if="sharedUserIds.length > 0" class="shared-users-items">
|
||||||
|
<div
|
||||||
|
v-for="userId in sharedUserIds"
|
||||||
|
:key="userId"
|
||||||
|
class="user-item shared-user"
|
||||||
|
>
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">{{ getUserName(userId) }}</span>
|
||||||
|
<span class="user-email">{{ getUserEmail(userId) }}</span>
|
||||||
|
</div>
|
||||||
|
<button @click="removeUser(userId)" class="remove-btn" :disabled="isUpdating">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-users">
|
||||||
|
{{ $t('visibility.noSharedUsers') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button @click="closeDialog" class="btn-cancel" :disabled="isUpdating">
|
<button @click="closeDialog" class="btn-cancel" :disabled="isUpdating">
|
||||||
{{ $t('visibility.cancel') }}
|
{{ $t('visibility.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="updateVisibility" class="btn-primary" :disabled="isUpdating">
|
<button @click="updateVisibilityAndShares" class="btn-primary" :disabled="isUpdating">
|
||||||
<span v-if="!isUpdating">{{ $t('visibility.save') }}</span>
|
<span v-if="!isUpdating">{{ $t('visibility.save') }}</span>
|
||||||
<span v-else>{{ $t('visibility.saving') }}</span>
|
<span v-else>{{ $t('visibility.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -75,7 +145,25 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
isUpdating: false
|
isUpdating: false,
|
||||||
|
isLoadingUsers: false,
|
||||||
|
availableUsers: [],
|
||||||
|
sharedUserIds: [],
|
||||||
|
searchQuery: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredAvailableUsers() {
|
||||||
|
let users = this.availableUsers.filter(user => !this.sharedUserIds.includes(user.user_id))
|
||||||
|
|
||||||
|
if (!this.searchQuery) {
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
const query = this.searchQuery.toLowerCase()
|
||||||
|
return users.filter(user =>
|
||||||
|
user.username.toLowerCase().includes(query) ||
|
||||||
|
user.email.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -88,21 +176,80 @@ export default {
|
|||||||
showDialog(newValue) {
|
showDialog(newValue) {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
this.isPublic = this.currentVisibility
|
this.isPublic = this.currentVisibility
|
||||||
|
this.loadAvailableUsers()
|
||||||
|
this.loadCurrentShares()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async loadAvailableUsers() {
|
||||||
|
this.isLoadingUsers = true
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.get(`/api/characters/${this.characterId}/available-users`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
this.availableUsers = response.data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load available users:', error)
|
||||||
|
} finally {
|
||||||
|
this.isLoadingUsers = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadCurrentShares() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.get(`/api/characters/${this.characterId}/shares`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
this.sharedUserIds = (response.data || []).map(share => share.user_id)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load current shares:', error)
|
||||||
|
this.sharedUserIds = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleUser(userId) {
|
||||||
|
const index = this.sharedUserIds.indexOf(userId)
|
||||||
|
if (index > -1) {
|
||||||
|
this.sharedUserIds.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
this.sharedUserIds.push(userId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeUser(userId) {
|
||||||
|
const index = this.sharedUserIds.indexOf(userId)
|
||||||
|
if (index > -1) {
|
||||||
|
this.sharedUserIds.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserName(userId) {
|
||||||
|
const user = this.availableUsers.find(u => u.user_id === userId)
|
||||||
|
return user ? user.username : 'Unknown'
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserEmail(userId) {
|
||||||
|
const user = this.availableUsers.find(u => u.user_id === userId)
|
||||||
|
return user ? user.email : ''
|
||||||
|
},
|
||||||
|
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
if (!this.isUpdating) {
|
if (!this.isUpdating) {
|
||||||
|
this.searchQuery = ''
|
||||||
this.$emit('update:showDialog', false)
|
this.$emit('update:showDialog', false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateVisibility() {
|
async updateVisibilityAndShares() {
|
||||||
this.isUpdating = true
|
this.isUpdating = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
// Update visibility
|
||||||
await API.patch(`/api/characters/${this.characterId}`,
|
await API.patch(`/api/characters/${this.characterId}`,
|
||||||
{ public: this.isPublic },
|
{ public: this.isPublic },
|
||||||
{
|
{
|
||||||
@@ -110,10 +257,18 @@ export default {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Update shares
|
||||||
|
await API.put(`/api/characters/${this.characterId}/shares`,
|
||||||
|
{ user_ids: this.sharedUserIds },
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
this.$emit('visibility-updated', this.isPublic)
|
this.$emit('visibility-updated', this.isPublic)
|
||||||
this.closeDialog()
|
this.closeDialog()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update character visibility:', error)
|
console.error('Failed to update character visibility/shares:', error)
|
||||||
alert(this.$t('visibility.updateError') + ': ' + (error.response?.data?.error || error.message))
|
alert(this.$t('visibility.updateError') + ': ' + (error.response?.data?.error || error.message))
|
||||||
} finally {
|
} finally {
|
||||||
this.isUpdating = false
|
this.isUpdating = false
|
||||||
@@ -178,4 +333,181 @@ export default {
|
|||||||
border-color: #007bff;
|
border-color: #007bff;
|
||||||
background-color: #e7f3ff;
|
background-color: #e7f3ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-large {
|
||||||
|
max-width: 700px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-sections-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-users-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-users-list h5 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-users-items {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.shared-user {
|
||||||
|
background: white;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.shared-user:hover {
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.shared-user .remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 8px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.shared-user .remove-btn:hover {
|
||||||
|
color: #a71d2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-chip .remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-chip .remove-btn:hover {
|
||||||
|
color: #a71d2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-users-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-users-section h5 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-users-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.selected {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 3px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-users {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+10
-1
@@ -527,7 +527,16 @@ export default {
|
|||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
saving: 'Wird gespeichert...',
|
saving: 'Wird gespeichert...',
|
||||||
updating: 'Sichtbarkeit wird aktualisiert...',
|
updating: 'Sichtbarkeit wird aktualisiert...',
|
||||||
updateError: 'Fehler beim Aktualisieren der Sichtbarkeit'
|
updateError: 'Fehler beim Aktualisieren der Sichtbarkeit',
|
||||||
|
shareWithUsers: 'Mit bestimmten Benutzern teilen',
|
||||||
|
shareDescription: 'Wählen Sie Benutzer aus, die diesen Charakter sehen können',
|
||||||
|
currentlySharedWith: 'Derzeit geteilt mit',
|
||||||
|
addUsers: 'Benutzer hinzufügen',
|
||||||
|
searchUsers: 'Benutzer suchen...',
|
||||||
|
loadingUsers: 'Lade Benutzer...',
|
||||||
|
noOtherUsers: 'Keine anderen Benutzer verfügbar',
|
||||||
|
noMatchingUsers: 'Keine Benutzer entsprechen Ihrer Suche',
|
||||||
|
noSharedUsers: 'Noch nicht mit Benutzern geteilt'
|
||||||
},
|
},
|
||||||
userManagement: {
|
userManagement: {
|
||||||
title: 'Benutzerverwaltung',
|
title: 'Benutzerverwaltung',
|
||||||
|
|||||||
+10
-1
@@ -523,7 +523,16 @@ export default {
|
|||||||
save: 'Save',
|
save: 'Save',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
updating: 'Updating visibility...',
|
updating: 'Updating visibility...',
|
||||||
updateError: 'Failed to update visibility'
|
updateError: 'Failed to update visibility',
|
||||||
|
shareWithUsers: 'Share with specific users',
|
||||||
|
shareDescription: 'Select users who can view this character',
|
||||||
|
currentlySharedWith: 'Currently shared with',
|
||||||
|
addUsers: 'Add users',
|
||||||
|
searchUsers: 'Search users...',
|
||||||
|
loadingUsers: 'Loading users...',
|
||||||
|
noOtherUsers: 'No other users available',
|
||||||
|
noMatchingUsers: 'No users match your search',
|
||||||
|
noSharedUsers: 'Not shared with any users yet'
|
||||||
},
|
},
|
||||||
userManagement: {
|
userManagement: {
|
||||||
title: 'User Management',
|
title: 'User Management',
|
||||||
|
|||||||
Reference in New Issue
Block a user