forked from trent_larson/crowd-funder-for-time-pwa
feat: implement member visibility dialog with checkbox selection and refresh
- Add "Set Visibility" dialog for meeting members who need visibility settings - Filter members to show only those not in contacts or without seesMe set - Implement checkbox selection with "Select All" functionality - Add reactive checkbox behavior with proper state management - Default all checkboxes to checked when dialog opens - Implement "Set Visibility" action to add contacts and set seesMe property - Add success notifications with count of affected members - Disable "Set Visibility" button when no members are selected - Use notification callbacks for data refresh - Hide "Set Visibility" buttons when no members need visibility settings - Add proper dialog state management and cleanup - Ensure dialog closes before triggering data refresh to prevent stale states The implementation provides a smooth user experience for managing member visibility settings with proper state synchronization between components.
This commit is contained in:
316
src/App.vue
316
src/App.vue
@@ -371,32 +371,65 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Custom table area - you can customize this -->
|
<!-- Custom table area - you can customize this -->
|
||||||
<div class="mb-4">
|
<div
|
||||||
|
v-if="shouldInitializeSelection(notification)"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
<table
|
<table
|
||||||
class="w-full border-collapse border border-slate-300 text-sm text-start"
|
class="w-full border-collapse border border-slate-300 text-sm text-start"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead
|
||||||
|
v-if="
|
||||||
|
notification.membersData &&
|
||||||
|
notification.membersData.length > 0
|
||||||
|
"
|
||||||
|
>
|
||||||
<tr class="bg-slate-100 font-medium">
|
<tr class="bg-slate-100 font-medium">
|
||||||
<th class="border border-slate-300 px-3 py-2">
|
<th class="border border-slate-300 px-3 py-2">
|
||||||
<label class="flex items-center gap-2">
|
<label class="flex items-center gap-2">
|
||||||
<input type="checkbox" checked />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="isAllSelected(notification)"
|
||||||
|
:indeterminate="isIndeterminate(notification)"
|
||||||
|
@change="toggleSelectAll(notification)"
|
||||||
|
/>
|
||||||
Select All
|
Select All
|
||||||
</label>
|
</label>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<!-- Sample data - replace with your actual data -->
|
<!-- Dynamic data from MembersList -->
|
||||||
<tr>
|
<tr
|
||||||
|
v-if="
|
||||||
|
!notification.membersData ||
|
||||||
|
notification.membersData.length === 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
|
||||||
|
>
|
||||||
|
No members need visibility settings
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="member in notification.membersData || []"
|
||||||
|
:key="member.member.memberId"
|
||||||
|
>
|
||||||
<td class="border border-slate-300 px-3 py-2">
|
<td class="border border-slate-300 px-3 py-2">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<label class="flex items-center gap-2">
|
<label class="flex items-center gap-2">
|
||||||
<input type="checkbox" checked />
|
<input
|
||||||
John Doe
|
type="checkbox"
|
||||||
|
:checked="isMemberSelected(member.did)"
|
||||||
|
@change="toggleMemberSelection(member.did)"
|
||||||
|
/>
|
||||||
|
{{ member.name || SOMEONE_UNNAMED }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- Friend indicator -->
|
<!-- Friend indicator - only show if they are already a contact -->
|
||||||
<font-awesome
|
<font-awesome
|
||||||
|
v-if="member.isContact"
|
||||||
icon="user-circle"
|
icon="user-circle"
|
||||||
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
|
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
|
||||||
@click="showContactInfo"
|
@click="showContactInfo"
|
||||||
@@ -404,62 +437,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td class="border border-slate-300 px-3 py-2">
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input type="checkbox" checked />
|
|
||||||
Jane Smith
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<!-- Friend indicator -->
|
|
||||||
<font-awesome
|
|
||||||
icon="user-circle"
|
|
||||||
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
|
|
||||||
@click="showContactInfo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="border border-slate-300 px-3 py-2">
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input type="checkbox" checked />
|
|
||||||
Jim Beam
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="border border-slate-300 px-3 py-2">
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input type="checkbox" checked />
|
|
||||||
Susie Q
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md"
|
v-if="
|
||||||
|
notification.membersData &&
|
||||||
|
notification.membersData.length > 0
|
||||||
|
"
|
||||||
|
:disabled="!hasSelectedMembers"
|
||||||
|
:class="[
|
||||||
|
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md',
|
||||||
|
hasSelectedMembers
|
||||||
|
? 'bg-blue-600 text-white cursor-pointer'
|
||||||
|
: 'bg-slate-400 text-slate-200 cursor-not-allowed',
|
||||||
|
]"
|
||||||
@click="
|
@click="
|
||||||
notification.onYes ? notification.onYes() : null;
|
setVisibilityForSelectedMembers(notification);
|
||||||
close(notification.id);
|
closeDialog(notification, close);
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Set Visibility
|
Set Visibility
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
@click="close(notification.id)"
|
@click="closeDialog(notification, close)"
|
||||||
>
|
>
|
||||||
Maybe Later
|
{{
|
||||||
|
notification.membersData &&
|
||||||
|
notification.membersData.length > 0
|
||||||
|
? "Maybe Later"
|
||||||
|
: "Cancel"
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,6 +488,9 @@ import { Vue, Component } from "vue-facing-decorator";
|
|||||||
import { NotificationIface } from "./constants/app";
|
import { NotificationIface } from "./constants/app";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
||||||
|
import { setVisibilityUtil } from "./libs/endorserServer";
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
notifyingNewActivityTime?: string;
|
notifyingNewActivityTime?: string;
|
||||||
@@ -491,6 +505,208 @@ export default class App extends Vue {
|
|||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
stopAsking = false;
|
stopAsking = false;
|
||||||
|
selectedMembers: string[] = [];
|
||||||
|
selectionInitialized = false;
|
||||||
|
|
||||||
|
get hasSelectedMembers() {
|
||||||
|
return this.selectedMembers.length > 0;
|
||||||
|
}
|
||||||
|
activeDid = "";
|
||||||
|
apiServer = "";
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
// Initialize settings
|
||||||
|
const settings = await this.$accountSettings();
|
||||||
|
this.apiServer = settings.apiServer || "";
|
||||||
|
|
||||||
|
// Get activeDid from active_identity table
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||||
|
this.activeDid = activeIdentity.activeDid || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllSelected(notification: NotificationIface) {
|
||||||
|
const membersData = notification?.membersData || [];
|
||||||
|
if (!membersData || membersData.length === 0) return false;
|
||||||
|
return membersData.every((member) =>
|
||||||
|
this.selectedMembers.includes(member.did),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isIndeterminate(notification: NotificationIface) {
|
||||||
|
const membersData = notification?.membersData || [];
|
||||||
|
if (!membersData || membersData.length === 0) return false;
|
||||||
|
const selectedCount = membersData.filter((member) =>
|
||||||
|
this.selectedMembers.includes(member.did),
|
||||||
|
).length;
|
||||||
|
return selectedCount > 0 && selectedCount < membersData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSelectAll(notification: NotificationIface) {
|
||||||
|
const membersData = notification?.membersData || [];
|
||||||
|
if (!membersData || membersData.length === 0) return;
|
||||||
|
|
||||||
|
if (this.isAllSelected(notification)) {
|
||||||
|
// Deselect all
|
||||||
|
this.selectedMembers = [];
|
||||||
|
} else {
|
||||||
|
// Select all
|
||||||
|
this.selectedMembers = membersData.map((member) => member.did);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMemberSelection(memberDid: string) {
|
||||||
|
const index = this.selectedMembers.indexOf(memberDid);
|
||||||
|
if (index > -1) {
|
||||||
|
this.selectedMembers.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedMembers.push(memberDid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isMemberSelected(memberDid: string) {
|
||||||
|
return this.selectedMembers.includes(memberDid);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldInitializeSelection(notification: NotificationIface) {
|
||||||
|
// This method will initialize selection when the dialog opens
|
||||||
|
if (
|
||||||
|
notification?.type === "set-visibility-meeting-members" &&
|
||||||
|
!this.selectionInitialized
|
||||||
|
) {
|
||||||
|
this.initializeSelection(notification);
|
||||||
|
this.selectionInitialized = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSelection(notification: NotificationIface) {
|
||||||
|
// Reset selection when dialog opens
|
||||||
|
this.selectedMembers = [];
|
||||||
|
// Select all by default
|
||||||
|
const membersData = notification?.membersData || [];
|
||||||
|
this.selectedMembers = membersData.map((member) => member.did);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSelection() {
|
||||||
|
this.selectedMembers = [];
|
||||||
|
this.selectionInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVisibilityForSelectedMembers(notification: NotificationIface) {
|
||||||
|
try {
|
||||||
|
const membersData = notification?.membersData || [];
|
||||||
|
const selectedMembers = membersData.filter((member) =>
|
||||||
|
this.selectedMembers.includes(member.did),
|
||||||
|
);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
for (const member of selectedMembers) {
|
||||||
|
try {
|
||||||
|
// If they're not a contact yet, add them as a contact first
|
||||||
|
if (!member.isContact) {
|
||||||
|
await this.addAsContact(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set their seesMe to true
|
||||||
|
await this.updateContactVisibility(member.did, true);
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Error processing member ${member.did}:`, error);
|
||||||
|
// Continue with other members even if one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Visibility Set Successfully",
|
||||||
|
text: `Visibility set for ${successCount} member${successCount === 1 ? "" : "s"}.`,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Error setting visibility:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to set visibility for some members. Please try again.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAsContact(member: { did: string; name: string }) {
|
||||||
|
try {
|
||||||
|
const newContact = {
|
||||||
|
did: member.did,
|
||||||
|
name: member.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.$insertContact(newContact);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Error adding contact:", err);
|
||||||
|
if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
|
||||||
|
// Contact already exists, continue
|
||||||
|
} else {
|
||||||
|
throw err; // Re-throw if it's not a duplicate error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateContactVisibility(did: string, seesMe: boolean) {
|
||||||
|
try {
|
||||||
|
// Get the contact object
|
||||||
|
const contact = await this.$getContact(did);
|
||||||
|
if (!contact) {
|
||||||
|
throw new Error(`Contact not found for DID: ${did}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the proper API to set visibility on the server
|
||||||
|
const result = await setVisibilityUtil(
|
||||||
|
this.activeDid,
|
||||||
|
this.apiServer,
|
||||||
|
this.axios,
|
||||||
|
contact,
|
||||||
|
seesMe,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Failed to set visibility");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Error updating contact visibility:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeDialog(
|
||||||
|
notification: NotificationIface,
|
||||||
|
closeFn: (id: string) => void,
|
||||||
|
) {
|
||||||
|
this.resetSelection();
|
||||||
|
|
||||||
|
// Close the notification first
|
||||||
|
closeFn(notification.id);
|
||||||
|
|
||||||
|
// Then call the callback after a short delay to ensure dialog is closed
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (notification.callback) {
|
||||||
|
await notification.callback();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
showContactInfo() {
|
showContactInfo() {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="getMembersForVisibility().length > 0"
|
||||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||||
title="Set visibility for meeting members"
|
title="Set visibility for meeting members"
|
||||||
@click="showAddMembersNotification"
|
@click="showAddMembersNotification"
|
||||||
@@ -162,6 +163,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="getMembersForVisibility().length > 0"
|
||||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||||
title="Set visibility for meeting members"
|
title="Set visibility for meeting members"
|
||||||
@click="showAddMembersNotification"
|
@click="showAddMembersNotification"
|
||||||
@@ -264,6 +266,12 @@ export default class MembersList extends Vue {
|
|||||||
await this.loadContacts();
|
await this.loadContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshData() {
|
||||||
|
// Force refresh both contacts and members
|
||||||
|
await this.loadContacts();
|
||||||
|
await this.fetchMembers();
|
||||||
|
}
|
||||||
|
|
||||||
async fetchMembers() {
|
async fetchMembers() {
|
||||||
try {
|
try {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
@@ -391,6 +399,28 @@ export default class MembersList extends Vue {
|
|||||||
return this.contacts.find((contact) => contact.did === did);
|
return this.contacts.find((contact) => contact.did === did);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMembersForVisibility() {
|
||||||
|
return this.decryptedMembers
|
||||||
|
.filter((member) => {
|
||||||
|
// Exclude the current user
|
||||||
|
if (member.did === this.activeDid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contact = this.getContactFor(member.did);
|
||||||
|
|
||||||
|
// Include members who:
|
||||||
|
// 1. Haven't been added as contacts yet, OR
|
||||||
|
// 2. Are contacts but don't have visibility set (seesMe property)
|
||||||
|
return !contact || !contact.seesMe;
|
||||||
|
})
|
||||||
|
.map((member) => ({
|
||||||
|
...member,
|
||||||
|
isContact: !!this.getContactFor(member.did),
|
||||||
|
contact: this.getContactFor(member.did),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
||||||
const contact = this.getContactFor(decrMember.did);
|
const contact = this.getContactFor(decrMember.did);
|
||||||
if (!decrMember.member.admitted && !contact) {
|
if (!decrMember.member.admitted && !contact) {
|
||||||
@@ -530,14 +560,17 @@ export default class MembersList extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showAddMembersNotification() {
|
showAddMembersNotification() {
|
||||||
|
// Filter members to show only those who need visibility set
|
||||||
|
const membersForVisibility = this.getMembersForVisibility();
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "modal",
|
group: "modal",
|
||||||
type: "set-visibility-meeting-members",
|
type: "set-visibility-meeting-members",
|
||||||
title: "Set Visibility for Meeting Members",
|
membersData: membersForVisibility, // Pass the filtered members data
|
||||||
onYes: async () => {
|
callback: async () => {
|
||||||
// Handle the "Add Selected" action - you can implement the actual logic here
|
// Refresh data when dialog is closed (regardless of action taken)
|
||||||
console.log("User confirmed adding selected members as contacts");
|
await this.refreshData();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const PASSKEYS_ENABLED =
|
|||||||
export interface NotificationIface {
|
export interface NotificationIface {
|
||||||
group: string; // "alert" | "modal"
|
group: string; // "alert" | "modal"
|
||||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||||
title: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
callback?: (success: boolean) => Promise<void>; // if this triggered an action
|
callback?: (success: boolean) => Promise<void>; // if this triggered an action
|
||||||
noText?: string;
|
noText?: string;
|
||||||
@@ -68,4 +68,11 @@ export interface NotificationIface {
|
|||||||
onYes?: () => Promise<void>;
|
onYes?: () => Promise<void>;
|
||||||
promptToStopAsking?: boolean;
|
promptToStopAsking?: boolean;
|
||||||
yesText?: string;
|
yesText?: string;
|
||||||
|
membersData?: Array<{
|
||||||
|
member: { admitted: boolean; content: string; memberId: number };
|
||||||
|
name: string;
|
||||||
|
did: string;
|
||||||
|
isContact: boolean;
|
||||||
|
contact?: { did: string; name?: string; seesMe?: boolean };
|
||||||
|
}>; // For passing member data to visibility dialog
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,9 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2
|
||||||
|
2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1
|
||||||
|
1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
Go Home
|
Go Home
|
||||||
|
|||||||
Reference in New Issue
Block a user