Modernize admin UI with React and update templates
This commit is contained in:
		
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							@@ -11,6 +11,7 @@ require (
 | 
			
		||||
	github.com/gabriel-vasile/mimetype v1.4.9 // indirect
 | 
			
		||||
	github.com/gin-contrib/cors v1.7.5 // indirect
 | 
			
		||||
	github.com/gin-contrib/sse v1.1.0 // indirect
 | 
			
		||||
	github.com/gin-contrib/static v1.1.5 // indirect
 | 
			
		||||
	github.com/gin-gonic/gin v1.10.0 // indirect
 | 
			
		||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							@@ -17,6 +17,8 @@ github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYM
 | 
			
		||||
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
 | 
			
		||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
 | 
			
		||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
 | 
			
		||||
github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4=
 | 
			
		||||
github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
 | 
			
		||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 | 
			
		||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
 | 
			
		||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/cors"
 | 
			
		||||
	"github.com/gin-contrib/static"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -19,27 +20,31 @@ func SetupRouter() *gin.Engine {
 | 
			
		||||
		AllowCredentials: true,
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	r.LoadHTMLGlob("internal/web/templates/**/*")
 | 
			
		||||
	r.LoadHTMLGlob("internal/web/templates/home/*")
 | 
			
		||||
 | 
			
		||||
	auth.InitJwtTokenGenerator() // Must be befor initializing admin handler, otherwise 'panic: runtime error: invalid memory address or nil pointer dereference'
 | 
			
		||||
	handlers.InitAdminDashboardHandler()
 | 
			
		||||
	handlers.InitApiEndpointHandler()
 | 
			
		||||
	handlers.InitEmbedCardHandler()
 | 
			
		||||
 | 
			
		||||
	// Main page
 | 
			
		||||
	r.GET("/", func(c *gin.Context) {
 | 
			
		||||
		c.HTML(http.StatusOK, "home/index.html", nil)
 | 
			
		||||
	})
 | 
			
		||||
	// Embed card
 | 
			
		||||
	r.GET("/embed", handlers.EmbedCardHandlerInstance.GetEmbedCard)
 | 
			
		||||
 | 
			
		||||
	admin := r.Group("/admin")
 | 
			
		||||
 | 
			
		||||
	// My man, this is done way more efficient and fast in .NET, specially the authentication part
 | 
			
		||||
	admin.GET("/", func(c *gin.Context) {
 | 
			
		||||
		c.HTML(http.StatusOK, "admin/index.html", nil)
 | 
			
		||||
	})
 | 
			
		||||
	admin.GET("/login", func(c *gin.Context) {
 | 
			
		||||
		c.HTML(http.StatusOK, "admin/login.html", nil)
 | 
			
		||||
	})
 | 
			
		||||
	// admin.GET("/", func(c *gin.Context) {
 | 
			
		||||
	// 	c.HTML(http.StatusOK, "admin/index.html", nil)
 | 
			
		||||
	// })
 | 
			
		||||
	// admin.GET("/login", func(c *gin.Context) {
 | 
			
		||||
	// 	c.HTML(http.StatusOK, "admin/index.html", nil)
 | 
			
		||||
	// })
 | 
			
		||||
	r.Use(static.Serve("/admin", static.LocalFile("internal/web/templates/admin", true)))
 | 
			
		||||
 | 
			
		||||
	adminApi := admin.Group("/api")
 | 
			
		||||
	adminApi.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
 | 
			
		||||
	adminApi.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								internal/web/templates/admin/assets/index-CWPfp0-e.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								internal/web/templates/admin/assets/index-CWPfp0-e.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								internal/web/templates/admin/assets/index-DShmOgsI.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								internal/web/templates/admin/assets/index-DShmOgsI.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								internal/web/templates/admin/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								internal/web/templates/admin/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 7.5 KiB  | 
@@ -1,156 +1,28 @@
 | 
			
		||||
{{ define "admin/index.html" }}
 | 
			
		||||
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
    <title>Media Approval</title>
 | 
			
		||||
    <style>
 | 
			
		||||
        body {
 | 
			
		||||
            font-family: sans-serif;
 | 
			
		||||
            background-color: #f9f9f9;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            height: 100vh;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
        }
 | 
			
		||||
        .card {
 | 
			
		||||
            background: white;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            max-width: 300px;
 | 
			
		||||
            position: relative;
 | 
			
		||||
        }
 | 
			
		||||
        .card img {
 | 
			
		||||
            max-width: 100%;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
        button {
 | 
			
		||||
            margin: 5px;
 | 
			
		||||
            padding: 10px 15px;
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
        .approve { background-color: #4CAF50; }
 | 
			
		||||
        .reject { background-color: #f44336; }
 | 
			
		||||
        .message {
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: -25px;
 | 
			
		||||
            left: 0;
 | 
			
		||||
            right: 0;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
            color: #4CAF50;
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Admin Dashboard</title>
 | 
			
		||||
    <meta name="description" content="Admin Dashboard for Media Review" />
 | 
			
		||||
    <meta name="author" content="Mahdium" />
 | 
			
		||||
 | 
			
		||||
<div class="card" id="media-card">
 | 
			
		||||
    <div class="message" id="status-msg"></div>
 | 
			
		||||
    <img id="preview" src="" alt="Media Preview" onerror="loadRemote()" />
 | 
			
		||||
    <div>
 | 
			
		||||
        <button class="approve" onclick="sendAction('approve')">Approve</button>
 | 
			
		||||
        <button class="reject" onclick="sendAction('reject')">Reject</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
    <meta property="og:title" content="Admin Dashboard" />
 | 
			
		||||
    <meta property="og:description" content="Admin Dashboard for Media Review" />
 | 
			
		||||
    <meta property="og:type" content="website" />
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
let currentMedia = null;
 | 
			
		||||
let nextMedia = null;
 | 
			
		||||
let preloadedImage = new Image();
 | 
			
		||||
    <meta name="twitter:card" content="summary_large_image" />
 | 
			
		||||
    <meta name="twitter:site" content="@mahdium86" />
 | 
			
		||||
    
 | 
			
		||||
    <script type="module" crossorigin src="/admin/assets/index-CWPfp0-e.js"></script>
 | 
			
		||||
    <link rel="stylesheet" crossorigin href="/admin/assets/index-DShmOgsI.css">
 | 
			
		||||
  </head>
 | 
			
		||||
 | 
			
		||||
window.onload = async () => {
 | 
			
		||||
    const token = getToken();
 | 
			
		||||
    if (!token) {
 | 
			
		||||
        alert("No token found. Please login first.");
 | 
			
		||||
        window.location.href = "login";
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    currentMedia = await fetchMedia(token);
 | 
			
		||||
    showMedia(currentMedia);
 | 
			
		||||
    preloadNextMedia(token);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getToken() {
 | 
			
		||||
    const match = document.cookie.match(/(^| )token=([^;]+)/);
 | 
			
		||||
    return match ? match[2] : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showMedia(media) {
 | 
			
		||||
    const preview = document.getElementById('preview');
 | 
			
		||||
    preview.src = media.preview_url;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function loadRemote() {
 | 
			
		||||
    if (currentMedia?.remote_url) {
 | 
			
		||||
        document.getElementById('preview').src = currentMedia.remote_url;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function sendAction(action) {
 | 
			
		||||
    if (!currentMedia?.id) return;
 | 
			
		||||
 | 
			
		||||
    const token = getToken();
 | 
			
		||||
    const endpoint = `api/${action}`;
 | 
			
		||||
    await fetch(endpoint, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
            'Content-Type': 'application/json',
 | 
			
		||||
            'Authorization': 'Bearer ' + token
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ mediaId: currentMedia.id })
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    showMessage(action.charAt(0).toUpperCase() + action.slice(1) + " successful");
 | 
			
		||||
 | 
			
		||||
    // Show next media
 | 
			
		||||
    if (nextMedia) {
 | 
			
		||||
        currentMedia = nextMedia;
 | 
			
		||||
        showMedia(currentMedia);
 | 
			
		||||
        preloadNextMedia(token);
 | 
			
		||||
    } else {
 | 
			
		||||
        document.getElementById('media-card').innerHTML = 'No more media.';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showMessage(text) {
 | 
			
		||||
    const msgDiv = document.getElementById('status-msg');
 | 
			
		||||
    msgDiv.innerText = text;
 | 
			
		||||
    msgDiv.style.display = 'block';
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        msgDiv.style.display = 'none';
 | 
			
		||||
    }, 500);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function fetchMedia(token) {
 | 
			
		||||
    const res = await fetch('api/getmedia', {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: { 'Authorization': 'Bearer ' + token }
 | 
			
		||||
    });
 | 
			
		||||
    return await res.json();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function preloadNextMedia(token) {
 | 
			
		||||
    try {
 | 
			
		||||
        nextMedia = await fetchMedia(token);
 | 
			
		||||
        preloadedImage.src = nextMedia.preview_url;
 | 
			
		||||
        preloadedImage.onerror = () => {
 | 
			
		||||
            preloadedImage.src = nextMedia.remote_url;
 | 
			
		||||
        };
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        console.error("Failed to preload next media:", err);
 | 
			
		||||
        nextMedia = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
 | 
			
		||||
    <script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
{{ end }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,92 +0,0 @@
 | 
			
		||||
{{ define "admin/login.html" }}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
    <title>Simple Login Page</title>
 | 
			
		||||
    <style>
 | 
			
		||||
        body {
 | 
			
		||||
            font-family: sans-serif;
 | 
			
		||||
            background-color: #f0f0f0;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            height: 100vh;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
        }
 | 
			
		||||
        .login-container {
 | 
			
		||||
            background-color: #fff;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
 | 
			
		||||
            text-align: center;
 | 
			
		||||
        }
 | 
			
		||||
        input[type="password"] {
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            margin: 10px 0;
 | 
			
		||||
            border: 1px solid #ccc;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            width: 200px;
 | 
			
		||||
        }
 | 
			
		||||
        button {
 | 
			
		||||
            background-color: #4CAF50;
 | 
			
		||||
            color: white;
 | 
			
		||||
            padding: 10px 15px;
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
        }
 | 
			
		||||
        button:hover {
 | 
			
		||||
            background-color: #3e8e41;
 | 
			
		||||
        }
 | 
			
		||||
        #message {
 | 
			
		||||
            margin-top: 10px;
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
<div class="login-container">
 | 
			
		||||
    <h2>Login</h2>
 | 
			
		||||
    <input type="password" id="password" placeholder="Password"><br>
 | 
			
		||||
    <button onclick="login()">Login</button>
 | 
			
		||||
    <div id="message"></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
function login() {
 | 
			
		||||
    const password = document.getElementById('password').value;
 | 
			
		||||
 | 
			
		||||
    fetch('api/login', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ "Password": password })
 | 
			
		||||
    })
 | 
			
		||||
    .then(response => response.json())
 | 
			
		||||
    .then(data => {
 | 
			
		||||
        if (data.message === "Login successful") {
 | 
			
		||||
            document.getElementById('message').innerText = data.message;
 | 
			
		||||
            saveToken(data.token);
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                window.location.href = "/admin/";
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        } else {
 | 
			
		||||
            document.getElementById('message').innerText = "Login failed.";
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
        console.error('Error:', error);
 | 
			
		||||
        document.getElementById('message').innerText = "An error occurred.";
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveToken(token) {
 | 
			
		||||
    document.cookie = "token=" + token + "; path=/";
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
{{ end }}
 | 
			
		||||
							
								
								
									
										1
									
								
								internal/web/templates/admin/placeholder.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								internal/web/templates/admin/placeholder.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.2 KiB  | 
		Reference in New Issue
	
	Block a user