Proje dosyaları eklendi
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.tp-status-switch{display:inline-flex;align-items:center;gap:.55rem;margin:0}.tp-status-switch .form-check-input{float:none;margin:0;cursor:pointer}.tp-api-table .dropdown-menu{max-width:min(18rem,calc(100vw - 2rem))}.tp-api-table .dropdown-item{white-space:normal;overflow-wrap:anywhere}
|
||||
@@ -0,0 +1 @@
|
||||
.tp-api-keys{min-width:0;overflow-x:clip}.tp-api-keys header>div{min-width:0;max-width:52rem}.tp-api-key-created,.tp-api-empty,.tp-code-example{overflow-wrap:anywhere}.tp-key-meta{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.tp-key-meta div{min-width:0}.tp-key-meta dt{color:var(--bs-secondary-color);font-size:.8rem}.tp-key-meta dd{margin:.2rem 0 0}.tp-key-icon{display:grid;place-items:center;width:3rem;height:3rem;margin:0 auto 1rem;border-radius:999px;background:rgba(var(--tp-brand-rgb),.12);color:rgb(var(--tp-brand-rgb));font-size:1.6rem}.tp-code-example{margin:0;padding:1rem;border-radius:.5rem;background:var(--bs-tertiary-bg);white-space:pre-wrap;word-break:break-word}.tp-api-keys :focus-visible{outline:.2rem solid rgba(var(--tp-brand-rgb),.45);outline-offset:.15rem}@media(max-width:767.98px){.tp-key-meta{grid-template-columns:1fr}.tp-api-table thead{display:none}.tp-api-table,.tp-api-table tbody,.tp-api-table tr,.tp-api-table td{display:block;width:100%}.tp-api-table tr{padding:.75rem 0;border-bottom:1px solid var(--bs-border-color)}.tp-api-table td{display:grid;grid-template-columns:minmax(7rem,38%) minmax(0,1fr);gap:.75rem;border:0;padding:.4rem .25rem;overflow-wrap:anywhere}.tp-api-table td::before{content:attr(data-label);color:var(--bs-secondary-color);font-size:.78rem;font-weight:700}.tp-api-keys .input-group{display:grid;grid-template-columns:1fr 1fr}.tp-api-keys .input-group .form-control{grid-column:1/-1;width:100%;border-radius:.375rem .375rem 0 0}.tp-api-keys .input-group .btn{border-radius:0}.tp-api-keys header .btn{width:100%}}
|
||||
@@ -0,0 +1,218 @@
|
||||
@import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
:root {
|
||||
--tp-brand-rgb: 91, 75, 255;
|
||||
--bs-primary-rgb: var(--tp-brand-rgb);
|
||||
--bs-link-color-rgb: var(--tp-brand-rgb);
|
||||
}
|
||||
|
||||
[data-bs-theme="light"] {
|
||||
--tp-page-accent: rgba(var(--tp-brand-rgb), .08);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
--tp-page-accent: rgba(var(--tp-brand-rgb), .16);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 85% 15%, var(--tp-page-accent), transparent 32rem),
|
||||
var(--bs-body-bg);
|
||||
}
|
||||
.tp-topbar{min-height:68px;position:sticky;top:0;z-index:1025}.tp-user-chip{align-items:center;gap:.65rem;padding:.25rem .6rem;border:1px solid var(--bs-border-color);border-radius:12px}.tp-user-chip span:last-child{display:flex;flex-direction:column;line-height:1.15}.tp-user-chip small{color:var(--bs-secondary-color);font-size:.7rem}.tp-avatar{display:grid;place-items:center;width:34px;height:34px;border-radius:10px;background:rgba(var(--tp-brand-rgb),.16);color:rgb(var(--tp-brand-rgb));font-weight:800}.tp-customer-shell{display:grid;grid-template-columns:260px minmax(0,1fr);min-height:calc(100vh - 68px)}.tp-customer-sidebar{background:linear-gradient(180deg,#111936,#0a1027);color:#d8def5;border:0!important;padding:1.25rem}.tp-customer-sidebar .offcanvas-body{padding:0}.tp-side-nav{display:grid;gap:.75rem}.tp-side-link,.tp-nav-section a,.tp-nav-section span{display:flex;align-items:center;gap:.65rem;color:#aeb8d8;text-decoration:none;padding:.65rem .75rem;border-radius:10px;font-size:.9rem}.tp-side-link.active,.tp-nav-section a.active{color:#fff;background:linear-gradient(90deg,rgba(91,75,255,.45),rgba(91,75,255,.12));box-shadow:inset 3px 0 #8d7cff}.tp-nav-section>button{width:100%;display:flex;justify-content:space-between;border:0;background:none;color:#fff;padding:.75rem;font-weight:700;text-align:left}.tp-nav-section .collapse{border-left:1px solid rgba(255,255,255,.12);margin-left:1rem;padding-left:.45rem}.tp-nav-section small{margin-left:auto;background:rgba(255,255,255,.08);padding:.1rem .35rem;border-radius:5px;font-size:.6rem}.tp-customer-main{padding:clamp(1rem,2.5vw,2.25rem);min-width:0;background:linear-gradient(145deg,var(--bs-body-bg),rgba(var(--tp-brand-rgb),.045))}.tp-page-header{display:flex;align-items:end;justify-content:space-between;gap:1.5rem;margin-bottom:1.5rem}.tp-page-header p,.tp-panel-heading p{margin:0;color:rgb(var(--tp-brand-rgb));font-weight:700;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem}.tp-page-header h1{font-size:clamp(1.7rem,3vw,2.35rem);margin:.15rem 0}.tp-page-header>div>span{color:var(--bs-secondary-color)}.tp-dashboard-filters{display:flex;gap:.75rem}.tp-dashboard-filters label{font-size:.72rem;color:var(--bs-secondary-color);font-weight:700}.tp-dashboard-filters select{min-width:150px;margin-top:.25rem}.tp-metric-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:1rem;margin-bottom:1rem}.tp-metric-card,.tp-panel{background:color-mix(in srgb,var(--bs-body-bg) 92%,rgb(var(--tp-brand-rgb)));border:1px solid color-mix(in srgb,var(--bs-border-color) 78%,rgb(var(--tp-brand-rgb)));border-radius:16px;box-shadow:0 12px 35px rgba(8,15,45,.07)}.tp-metric-card{padding:1.15rem;display:flex;align-items:center;gap:.8rem;min-width:0}.tp-metric-icon{display:grid;place-items:center;flex:0 0 40px;height:40px;border-radius:12px;background:rgba(var(--tp-brand-rgb),.13);color:rgb(var(--tp-brand-rgb));font-size:1.25rem}.tp-metric-card p{margin:0;color:var(--bs-secondary-color);font-size:.72rem}.tp-metric-card strong{font-size:1.6rem}.tp-panel{padding:1.25rem;margin-bottom:1rem;overflow:hidden}.tp-panel-heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem}.tp-panel-heading h2{font-size:1.05rem;margin:.2rem 0 0}.tp-chart-panel{min-height:280px}.tp-rank-bars{display:grid;gap:.65rem}.tp-rank-bars>div{display:grid;grid-template-columns:minmax(110px,180px) 1fr 45px;align-items:center;gap:.8rem;font-size:.8rem}.tp-rank-bars i{display:block;height:9px;border-radius:9px;background:linear-gradient(90deg,#6d5dfc,#16c7a3);width:calc((101 - var(--rank)) * 1%);min-width:4px}.tp-data-table{--bs-table-bg:transparent;margin:0}.tp-data-table th{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:var(--bs-secondary-color);white-space:nowrap}.tp-data-table td{vertical-align:middle;white-space:nowrap}.tp-rank-up{color:#14a879;font-weight:700}.tp-rank-down{color:#e45175;font-weight:700}.tp-empty-state{text-align:center;padding:2rem;color:var(--bs-secondary-color)}.tp-empty-state>div{font-size:2rem;color:rgb(var(--tp-brand-rgb))}.tp-empty-state h3{font-size:1rem;color:var(--bs-body-color);margin:.5rem}.tp-empty-state p{margin:0;font-size:.85rem}.tp-two-columns{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.tp-mover{display:flex;justify-content:space-between;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--bs-border-color)}.tp-mover span:first-child{display:flex;flex-direction:column}.tp-mover small{color:var(--bs-secondary-color)}.tp-sidebar-toggle{position:fixed;left:.75rem;top:.8rem;z-index:1040}.tp-mobile-menu-slot{width:42px}
|
||||
|
||||
.tp-brand-logo-link {
|
||||
display: inline-flex;
|
||||
max-width: min(52vw, 19rem);
|
||||
flex: 0 1 auto;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
.tp-brand-logo-link:focus-visible { outline: .2rem solid rgba(var(--tp-brand-rgb), .45); outline-offset: .2rem; }
|
||||
.tp-brand-logo { display: inline-block; height: var(--tp-logo-height); max-width: 100%; aspect-ratio: 2172 / 724; }
|
||||
.tp-brand-logo img { display: block; width: auto; height: 100%; max-width: 100%; object-fit: contain; object-position: center; }
|
||||
.tp-brand-logo-dark { display: none !important; }
|
||||
[data-bs-theme="dark"] .tp-brand-logo-light { display: none !important; }
|
||||
[data-bs-theme="dark"] .tp-brand-logo-dark { display: block !important; }
|
||||
|
||||
.tp-sidebar { min-height: calc(100vh - 73px); }
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.tp-sidebar { min-height: auto; }
|
||||
.tp-customer-shell{display:block}.tp-customer-sidebar{width:min(86vw,290px)}.tp-metric-grid{grid-template-columns:repeat(3,1fr)}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.tp-brand-logo-link { max-width: 42vw; }
|
||||
.tp-brand-logo-auth { --tp-logo-height: 60px !important; }
|
||||
.tp-page-header,.tp-dashboard-filters{align-items:stretch;flex-direction:column}.tp-dashboard-filters select{min-width:0}.tp-metric-grid{grid-template-columns:1fr 1fr}.tp-two-columns{grid-template-columns:1fr}.tp-customer-main{padding:.85rem}
|
||||
}
|
||||
|
||||
.tp-public-topbar { min-width: 0; min-height: 52px; flex-wrap: nowrap; }
|
||||
.tp-public-topbar .tp-brand-logo-link {
|
||||
display: block;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: min(11.25rem, calc(100vw - 5rem));
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.tp-public-topbar .tp-brand-logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 36px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.tp-public-topbar .tp-brand-logo img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
.tp-public-desktop-actions { min-width: 0; }
|
||||
.tp-public-desktop-actions .btn,
|
||||
.tp-public-mobile-menu .btn { white-space: nowrap; word-break: normal; overflow-wrap: normal; }
|
||||
.tp-public-menu-toggle { flex: 0 0 auto; width: 2.5rem; height: 2.5rem; padding: .35rem; }
|
||||
.tp-public-menu-toggle:focus-visible { outline: .2rem solid rgba(var(--tp-brand-rgb), .45); outline-offset: .15rem; box-shadow: none; }
|
||||
.tp-public-mobile-menu { width: min(20rem, 100vw); max-width: 100%; }
|
||||
.tp-public-mobile-menu .offcanvas-body { min-width: 0; }
|
||||
.tp-public-mobile-menu .offcanvas-body > .btn { width: 100%; min-height: 2.75rem; text-align: start; }
|
||||
.tp-public-mobile-menu .dropdown,
|
||||
.tp-public-mobile-menu .dropdown-toggle { min-width: 0; max-width: 100%; }
|
||||
.tp-public-mobile-menu .dropdown-menu { width: max-content; max-width: calc(100vw - 2rem); }
|
||||
.tp-public-mobile-menu .dropdown-item { white-space: normal; overflow-wrap: break-word; }
|
||||
.tp-public-mobile-preferences { display: grid !important; grid-template-columns: minmax(0, auto) minmax(0, 1fr); flex-wrap: wrap; }
|
||||
.tp-public-mobile-preferences .tp-theme-picker { justify-self: end; min-width: 0; }
|
||||
.tp-public-mobile-preferences .tp-theme-picker .dropdown-toggle { width: 100%; }
|
||||
.tp-public-mobile-preferences .btn { min-height: 2.375rem; padding: .35rem .6rem; }
|
||||
.tp-public-mobile-preferences .tp-theme-picker .btn { gap: .35rem !important; }
|
||||
@media (max-width: 575.98px) {
|
||||
.tp-public-topbar .tp-brand-logo-link { display: block; }
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.tp-public-topbar .tp-brand-logo { height: 40px; }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.tp-public-topbar .tp-brand-logo { height: 44px; }
|
||||
.tp-public-topbar .tp-brand-logo-link { max-width: 13.5rem; }
|
||||
}
|
||||
|
||||
.tp-auth-page { width: 100%; padding-right: 1rem; padding-left: 1rem; }
|
||||
.tp-auth-column,.tp-auth-card,.tp-auth-card .card-body,.tp-auth-card form { min-width: 0; max-width: 100%; }
|
||||
.tp-auth-card .form-control,.tp-auth-card .form-select,.tp-auth-card .btn { max-width: 100%; }
|
||||
.tp-auth-card h1,.tp-auth-card p,.tp-auth-card .alert,.tp-auth-card .form-check-label { overflow-wrap: break-word; }
|
||||
.tp-auth-card .form-check { min-width: 0; }
|
||||
.tp-auth-card .tp-brand-logo-link { max-width: 100%; }
|
||||
.tp-auth-links { min-width: 0; }
|
||||
@media (max-width: 359.98px) {
|
||||
.tp-auth-links { align-items: flex-start; flex-direction: column; }
|
||||
.tp-public-mobile-preferences { grid-template-columns: 1fr; }
|
||||
.tp-public-mobile-preferences .tp-theme-picker { justify-self: stretch; }
|
||||
}
|
||||
|
||||
.card,
|
||||
.navbar,
|
||||
.dropdown-menu,
|
||||
.btn {
|
||||
transition: background-color .2s ease, border-color .2s ease, color .2s ease;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.card,
|
||||
.navbar,
|
||||
.dropdown-menu,
|
||||
.btn {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tp-project-metrics { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
.tp-detail-list { margin: 0; }
|
||||
.tp-detail-list > div { display: grid; grid-template-columns: minmax(110px, 35%) 1fr; gap: 1rem; padding: .8rem 0; border-bottom: 1px solid var(--bs-border-color); }
|
||||
.tp-detail-list > div:last-child { border: 0; }
|
||||
.tp-detail-list dt { color: var(--bs-secondary-color); font-size: .8rem; }
|
||||
.tp-detail-list dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
.tp-technical-detail { opacity: .75; }
|
||||
.tp-form-panel { max-width: 760px; }
|
||||
.tp-keyword-metrics{grid-template-columns:repeat(6,minmax(0,1fr))}.tp-keyword-filters{display:grid;grid-template-columns:minmax(180px,2fr) repeat(3,minmax(130px,1fr)) auto;gap:.75rem}.tp-history-list{display:grid;gap:.75rem}.tp-history-list>div{display:grid;grid-template-columns:100px 1fr 45px;align-items:center;gap:.75rem}.tp-history-list span{height:9px;border-radius:9px;background:linear-gradient(90deg,#6d5dfc,#16c7a3);width:calc((101 - var(--rank))*1%);min-width:4px}
|
||||
@media(max-width:991.98px){.tp-keyword-filters{grid-template-columns:1fr 1fr}.tp-keyword-metrics{grid-template-columns:repeat(3,1fr)}}
|
||||
@media(max-width:575.98px){.tp-keyword-filters,.tp-keyword-metrics{grid-template-columns:1fr}.tp-panel{padding:.85rem}.tp-data-table td,.tp-data-table th{font-size:.78rem}}
|
||||
|
||||
/* Customer navigation starts at 20px; its only brand mark lives in the topbar. */
|
||||
.tp-sidebar-closebar{position:absolute;top:.75rem;right:.75rem;z-index:2;padding:0}
|
||||
@media(max-width:575.98px){.tp-customer-top-logo{margin-left:3.25rem}.tp-customer-top-logo .tp-brand-logo-link{display:inline-flex}}
|
||||
.tp-user-chip{color:var(--bs-body-color);text-decoration:none;min-width:0;max-width:280px;transition:background-color .15s,border-color .15s}.tp-user-chip:hover{background:var(--bs-secondary-bg);border-color:rgb(var(--tp-brand-rgb))}.tp-user-chip:focus-visible{outline:3px solid rgba(var(--tp-brand-rgb),.35);outline-offset:2px}.tp-user-chip-copy{display:flex;flex-direction:column;line-height:1.15;min-width:0}.tp-user-chip strong,.tp-user-chip small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.tp-ranking-filters{display:grid;grid-template-columns:repeat(3,minmax(140px,1fr)) repeat(2,minmax(130px,.8fr));gap:.75rem}.tp-ranking-metrics{grid-template-columns:repeat(6,minmax(0,1fr))}.tp-ranking-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(250px,1fr);gap:1rem}.tp-ranking-chart{width:100%;overflow:hidden}.tp-ranking-chart svg{display:block;width:100%;min-height:260px}.tp-ranking-chart polyline{fill:none;stroke:var(--series);stroke-width:3}.tp-ranking-chart circle{fill:var(--bs-body-bg);stroke:var(--series);stroke-width:3}.tp-chart-grid line{stroke:var(--bs-border-color);stroke-width:1}.tp-chart-grid text{fill:var(--bs-secondary-color);font-size:12px}.tp-chart-legend{display:flex;flex-wrap:wrap;gap:.75rem;font-size:.75rem}.tp-chart-legend span{display:flex;align-items:center;gap:.3rem}.tp-chart-legend i{width:10px;height:10px;border-radius:50%}.tp-distribution{display:grid;gap:.75rem}.tp-distribution>div{display:grid;grid-template-columns:90px 1fr 30px;align-items:center;gap:.5rem;font-size:.8rem}.tp-distribution i{height:8px;width:min(100%,calc(var(--count)*12%));min-width:3px;border-radius:8px;background:linear-gradient(90deg,#6d5dfc,#16c7a3)}.tp-movement{font-weight:700}.tp-winner{color:#148563}.tp-loser{color:#c83c61}.tp-organic-result{display:grid;gap:.2rem;padding:.7rem 0;border-bottom:1px solid var(--bs-border-color);overflow-wrap:anywhere}
|
||||
@media(max-width:991.98px){.tp-ranking-filters{grid-template-columns:1fr 1fr}.tp-ranking-metrics{grid-template-columns:repeat(3,1fr)}.tp-ranking-grid{grid-template-columns:1fr}}
|
||||
@media(max-width:575.98px){.tp-ranking-filters,.tp-ranking-metrics{grid-template-columns:1fr}.tp-ranking-chart svg{min-width:0;height:auto}.tp-ranking-grid{min-width:0}}
|
||||
.tp-serp-filters{display:grid;grid-template-columns:repeat(3,minmax(140px,1fr));gap:.75rem}.tp-serp-metrics{grid-template-columns:repeat(6,minmax(0,1fr))}.tp-status-pending{background:#806f2a;color:#fff}.tp-status-running{background:#2867a8;color:#fff}.tp-status-completed{background:#14785d;color:#fff}.tp-status-failed{background:#ad3655;color:#fff}
|
||||
@media(max-width:991.98px){.tp-serp-filters{grid-template-columns:1fr 1fr}.tp-serp-metrics{grid-template-columns:repeat(3,1fr)}}@media(max-width:575.98px){.tp-serp-filters,.tp-serp-metrics{grid-template-columns:1fr}}
|
||||
.tp-visibility-filters{display:grid;grid-template-columns:repeat(3,minmax(140px,1fr));gap:.75rem}.tp-visibility-chart svg{display:block;width:100%;min-height:260px}.tp-visibility-chart polyline{fill:none;stroke-width:3}.tp-visibility-chart circle{fill:var(--bs-body-bg);stroke:var(--series);stroke-width:3}@media(max-width:991.98px){.tp-visibility-filters{grid-template-columns:1fr 1fr}}@media(max-width:575.98px){.tp-visibility-filters{grid-template-columns:1fr}.tp-visibility-chart svg{min-width:0;height:auto}}
|
||||
.tp-metric-grid,.tp-project-metrics,.tp-keyword-metrics,.tp-ranking-metrics,.tp-serp-metrics,.tp-visibility-metrics{grid-template-columns:repeat(auto-fit,minmax(min(190px,100%),1fr))}.tp-metric-card{min-height:96px;min-width:0}.tp-metric-content{display:flex;flex:1 1 auto;min-width:0;flex-direction:column;justify-content:center}.tp-metric-label{white-space:normal;overflow-wrap:break-word;line-height:1.3}.tp-metric-value{display:block;min-width:0;white-space:nowrap;line-height:1.15}.tp-metric-description{display:block;margin-top:.22rem;color:var(--bs-secondary-color);font-size:.8125rem;line-height:1.35;white-space:normal;overflow-wrap:break-word}@media(max-width:767.98px) and (min-width:576px){.tp-metric-grid,.tp-project-metrics,.tp-keyword-metrics,.tp-ranking-metrics,.tp-serp-metrics,.tp-visibility-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:575.98px){.tp-metric-grid,.tp-project-metrics,.tp-keyword-metrics,.tp-ranking-metrics,.tp-serp-metrics,.tp-visibility-metrics{grid-template-columns:minmax(0,1fr)}}
|
||||
.tp-winners-filters{display:grid;grid-template-columns:repeat(3,minmax(140px,1fr));gap:.75rem}.tp-winners-metrics{grid-template-columns:repeat(auto-fit,minmax(min(190px,100%),1fr))}@media(max-width:767.98px){.tp-winners-filters{grid-template-columns:1fr 1fr}}@media(max-width:575.98px){.tp-winners-filters,.tp-winners-metrics{grid-template-columns:minmax(0,1fr)}}
|
||||
.tp-losers-filters{display:grid;grid-template-columns:repeat(3,minmax(140px,1fr));gap:.75rem}.tp-losers-metrics{grid-template-columns:repeat(auto-fit,minmax(min(190px,100%),1fr))}.tp-critical-losses{border-color:color-mix(in srgb,#c83c61 35%,var(--bs-border-color))}.tp-distribution .tp-loss-bar{background:linear-gradient(90deg,#c83c61,#ef718e)}@media(max-width:767.98px){.tp-losers-filters{grid-template-columns:1fr 1fr}}@media(max-width:575.98px){.tp-losers-filters,.tp-losers-metrics{grid-template-columns:minmax(0,1fr)}}
|
||||
.tp-competitor-form{display:grid;grid-template-columns:1fr 1fr auto;gap:.75rem}@media(max-width:575.98px){.tp-competitor-form{grid-template-columns:1fr}}
|
||||
.tp-profile-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.tp-profile-grid>*{min-width:0}.tp-profile-grid .form-label{display:block;margin-bottom:.8rem}.tp-profile-sessions{grid-column:1/-1}.tp-session-row{display:flex;justify-content:space-between;gap:1rem;padding:.6rem 0;border-bottom:1px solid var(--bs-border-color);overflow-wrap:anywhere}@media(max-width:767.98px){.tp-profile-grid{grid-template-columns:1fr}.tp-profile-sessions{grid-column:auto}.tp-session-row{flex-direction:column;gap:.2rem}}
|
||||
.tp-settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.tp-settings-grid>*{min-width:0}.tp-settings-grid .form-label{display:block;margin-bottom:.8rem}.tp-settings-reset{grid-column:1/-1}html[data-reduced-motion="true"] *{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}}@media(max-width:767.98px){.tp-settings-grid{grid-template-columns:1fr}.tp-settings-reset{grid-column:auto}}
|
||||
|
||||
.tp-landing { overflow-x: clip; }
|
||||
.tp-landing-hero { padding: clamp(4rem, 9vw, 7.5rem) 0; background: color-mix(in srgb, #111936 94%, var(--bs-body-bg)); color: #f7f8ff; }
|
||||
.tp-landing-hero .container { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(18rem, .8fr); align-items: center; gap: clamp(2rem, 6vw, 5rem); }
|
||||
.tp-landing-hero-copy { max-width: 48rem; }
|
||||
.tp-landing-eyebrow,.tp-section-heading>span { display: inline-block; margin-bottom: .8rem; color: #8f9cff; font-size: .75rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.tp-landing-hero h1 { margin: 0; max-width: 52rem; font-size: clamp(2.15rem, 5vw, 4.4rem); line-height: 1.06; letter-spacing: -.035em; overflow-wrap: break-word; }
|
||||
.tp-landing-hero-copy>p { max-width: 44rem; margin: 1.4rem 0 0; color: #c8cee6; font-size: clamp(1rem, 1.6vw, 1.18rem); line-height: 1.7; }
|
||||
.tp-landing-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2rem; }
|
||||
.tp-landing-actions .btn-primary,.tp-landing-cta .btn-primary { background: #6d5dfc; border-color: #6d5dfc; }
|
||||
.tp-landing-actions .btn-outline-secondary { color: #f7f8ff; border-color: #66708f; }
|
||||
.tp-landing-preview { min-width: 0; padding: 1.5rem; border: 1px solid #384260; border-radius: 1rem; background: #161f3e; box-shadow: 0 1.5rem 4rem rgba(4,8,24,.28); }
|
||||
.tp-preview-heading,.tp-preview-metric,.tp-preview-status { display: flex; justify-content: space-between; gap: 1rem; }
|
||||
.tp-preview-heading { color: #aeb8d8; font-size: .8rem; }
|
||||
.tp-preview-heading strong { color: #fff; }
|
||||
.tp-preview-metric { align-items: end; margin-top: 2rem; color: #aeb8d8; }
|
||||
.tp-preview-metric strong { color: #fff; font-size: 2.35rem; line-height: 1; }
|
||||
.tp-preview-chart { display: flex; align-items: end; gap: .6rem; height: 9rem; margin: 1.5rem 0; border-bottom: 1px solid #384260; }
|
||||
.tp-preview-chart i { width: 100%; border-radius: .3rem .3rem 0 0; background: #6d5dfc; }
|
||||
.tp-preview-chart i:nth-child(1){height:32%}.tp-preview-chart i:nth-child(2){height:48%}.tp-preview-chart i:nth-child(3){height:42%}.tp-preview-chart i:nth-child(4){height:65%}.tp-preview-chart i:nth-child(5){height:74%}.tp-preview-chart i:nth-child(6){height:88%}
|
||||
.tp-preview-status { color: #aeb8d8; font-size: .75rem; }.tp-preview-status span:first-child { color: #53d2ae; }
|
||||
.tp-landing-section { padding: clamp(3.5rem, 7vw, 6rem) 0; }
|
||||
.tp-section-heading { max-width: 48rem; margin-bottom: 2rem; }.tp-section-heading h2 { margin: 0; font-size: clamp(1.8rem, 3.5vw, 2.8rem); }.tp-section-heading p { margin: .9rem 0 0; color: var(--bs-secondary-color); font-size: 1.05rem; }
|
||||
.tp-feature-grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 1rem; }
|
||||
.tp-feature-card { min-width: 0; padding: 1.35rem; border: 1px solid var(--bs-border-color); border-radius: 1rem; background: var(--bs-body-bg); }
|
||||
.tp-feature-icon { display: grid; place-items: center; width: 2.5rem; height: 2.5rem; border-radius: .7rem; background: rgba(var(--tp-brand-rgb),.12); color: rgb(var(--tp-brand-rgb)); font-size: 1.25rem; }
|
||||
.tp-feature-card h3 { margin: 1rem 0 .5rem; font-size: 1.02rem; }.tp-feature-card p { margin: 0; color: var(--bs-secondary-color); font-size: .9rem; line-height: 1.6; }
|
||||
.tp-how-section { background: var(--bs-tertiary-bg); }.tp-how-grid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 1rem; margin: 0; padding: 0; list-style: none; }
|
||||
.tp-how-grid li { display: flex; gap: 1rem; min-width: 0; padding: 1.5rem; }.tp-how-grid>li>span { display: grid; place-items: center; flex: 0 0 2.5rem; height: 2.5rem; border-radius: 999px; background: #111936; color: #fff; font-weight: 800; }.tp-how-grid h3 { margin: .35rem 0 .5rem; font-size: 1.05rem; }.tp-how-grid p { margin: 0; color: var(--bs-secondary-color); line-height: 1.6; }
|
||||
.tp-landing-cta { padding: 3rem 0; background: #111936; color: #fff; }.tp-landing-cta .container { display: flex; align-items: center; justify-content: space-between; gap: 2rem; }.tp-landing-cta h2 { margin: 0; font-size: clamp(1.5rem,3vw,2.25rem); }.tp-landing-cta p { margin: .65rem 0 0; color: #c8cee6; }
|
||||
.tp-landing-footer { padding: 1.5rem 0; border-top: 1px solid var(--bs-border-color); }.tp-landing-footer .container { display: flex; align-items: center; gap: 1.5rem; }.tp-footer-brand { color: var(--bs-body-color); font-weight: 800; text-decoration: none; }.tp-landing-footer nav { display: flex; gap: 1rem; margin-left: auto; }.tp-landing-footer a { text-underline-offset: .2rem; }.tp-landing-footer small { color: var(--bs-secondary-color); }
|
||||
@media(max-width:991.98px){.tp-landing-hero .container{grid-template-columns:1fr}.tp-landing-preview{max-width:40rem}.tp-feature-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:575.98px){.tp-landing-hero{padding:3.5rem 0}.tp-landing-actions{display:grid;grid-template-columns:1fr}.tp-landing-actions .btn{width:100%}.tp-feature-grid,.tp-how-grid{grid-template-columns:1fr}.tp-how-grid li{padding:1rem 0}.tp-landing-cta .container,.tp-landing-footer .container{align-items:flex-start;flex-direction:column}.tp-landing-footer nav{margin-left:0}.tp-landing-preview{padding:1rem}.tp-preview-status{flex-direction:column;gap:.35rem}}
|
||||
|
||||
.tp-earn { overflow-x: clip; }.tp-earn-hero { padding: clamp(4rem,8vw,7rem) 0; background:#111936; color:#fff; }.tp-earn-hero .container { display:grid; grid-template-columns:minmax(0,1fr) minmax(18rem,.8fr); align-items:center; gap:clamp(2rem,6vw,5rem); }.tp-earn-hero h1 { margin:0; max-width:48rem; font-size:clamp(2.1rem,5vw,4.1rem); line-height:1.08; letter-spacing:-.035em; }.tp-earn-hero-copy>p { max-width:43rem; margin:1.3rem 0 0; color:#c8cee6; font-size:clamp(1rem,1.6vw,1.15rem); line-height:1.7; }.tp-coming-soon { display:inline-flex; margin-top:1rem; padding:.35rem .7rem; border:1px solid #4d5879; border-radius:999px; color:#c8cee6; font-size:.75rem; font-weight:700; }.tp-coming-soon-light { margin:0; color:#fff; }
|
||||
.tp-device-scene { position:relative; min-height:22rem; }.tp-device { position:absolute; border:2px solid #596583; background:#161f3e; box-shadow:0 1.5rem 4rem rgba(4,8,24,.3); }.tp-device>span { display:block; background:#090f25; }.tp-device>div { display:grid; gap:.7rem; padding:1.2rem; }.tp-device i { display:block; height:.65rem; border-radius:1rem; background:#374261; }.tp-device i:nth-child(2){width:76%;background:#6d5dfc}.tp-device strong { color:#dce1f4; font-size:.8rem; }.tp-device-desktop { inset:1rem 0 3rem 0; border-radius:1rem; }.tp-device-desktop>span { height:1.5rem; border-radius:.8rem .8rem 0 0; }.tp-device-mobile { right:-.5rem; bottom:0; width:8.5rem; min-height:13rem; border-radius:1.4rem; }.tp-device-mobile>span { width:2.5rem; height:.35rem; margin:.55rem auto; border-radius:1rem; }
|
||||
.tp-earn-rate { padding:2rem 0; background:#15213e; color:#fff; }.tp-earn-rate .container { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:2rem; }.tp-earn-rate span { color:#62d7b5; font-size:.75rem; font-weight:800; letter-spacing:.08em; text-transform:uppercase; }.tp-earn-rate h2 { margin:.35rem 0; font-size:clamp(1.7rem,4vw,3rem); }.tp-earn-rate p { max-width:45rem; margin:0; color:#c8cee6; }.tp-earn-rate dl { display:flex; gap:2rem; margin:0; }.tp-earn-rate dl div { min-width:10rem; }.tp-earn-rate dt { color:#aeb8d8; font-size:.75rem; }.tp-earn-rate dd { margin:.3rem 0 0; font-size:1.25rem; font-weight:800; }
|
||||
.tp-earn-process { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; margin:0; padding:0; list-style:none; counter-reset:step; }.tp-earn-process li { display:flex; gap:1rem; min-width:0; padding:1.25rem; border:1px solid var(--bs-border-color); border-radius:1rem; }.tp-earn-process>li>span { display:grid; place-items:center; flex:0 0 2.4rem; height:2.4rem; border-radius:999px; background:#6d5dfc; color:#fff; font-weight:800; }.tp-earn-process h3 { margin:.2rem 0 .5rem; font-size:1rem; }.tp-earn-process p { margin:0; color:var(--bs-secondary-color); font-size:.88rem; line-height:1.55; }
|
||||
.tp-earn-calculation { background:var(--bs-tertiary-bg); }.tp-calculation-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; }.tp-calculation-card { display:grid; grid-template-columns:1fr auto; align-items:center; gap:.35rem 1rem; min-width:0; padding:1.5rem; border:1px solid var(--bs-border-color); border-radius:1rem; background:var(--bs-body-bg); }.tp-calculation-card strong { font-size:clamp(1.4rem,3vw,2rem); }.tp-calculation-card span { color:var(--bs-secondary-color); }.tp-calculation-card i { grid-row:1/3; grid-column:2; color:rgb(var(--tp-brand-rgb)); font-style:normal; font-size:1.4rem; }.tp-calculation-card b { grid-column:1/-1; margin-top:.7rem; color:#18a47e; font-size:1.35rem; }.tp-calculation-card small { grid-column:1/-1; color:#18a47e; }.tp-payout-card { border-color:#18a47e; }.tp-earn-disclosures { display:grid; gap:.6rem; margin:1.5rem 0 0; color:var(--bs-secondary-color); }
|
||||
.tp-safety-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.8rem 1.5rem; margin:0; padding:0; list-style:none; }.tp-safety-grid li { display:flex; align-items:flex-start; gap:.65rem; min-width:0; line-height:1.55; }.tp-safety-grid li span { color:#18a47e; font-weight:900; }
|
||||
@media(max-width:991.98px){.tp-earn-hero .container{grid-template-columns:1fr}.tp-device-scene{max-width:36rem}.tp-earn-rate .container{grid-template-columns:1fr}.tp-earn-process{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:575.98px){.tp-earn-hero{padding:3.5rem 0}.tp-device-scene{min-height:18rem}.tp-device-desktop{right:1rem}.tp-device-mobile{right:0}.tp-earn-rate dl{display:grid;gap:1rem}.tp-earn-process,.tp-calculation-grid,.tp-safety-grid{grid-template-columns:1fr}.tp-earn-process li{padding:1rem}.tp-calculation-card{padding:1.15rem}}
|
||||
/* Earn legal drafts: readable long-form content without horizontal overflow. */
|
||||
.tp-legal-document { overflow-wrap: anywhere; }
|
||||
|
||||
/* Keep the Earn device illustration and translated copy inside their grid track. */
|
||||
.tp-device-scene { width: 100%; max-width: 100%; min-width: 0; }
|
||||
.tp-device { max-width: 100%; }
|
||||
@media (max-width: 575.98px) {
|
||||
.tp-earn-hero h1 { font-size: clamp(1.9rem, 10vw, 2.65rem); overflow-wrap: break-word; }
|
||||
.tp-earn-hero-copy,.tp-earn-hero .container > * { min-width: 0; }
|
||||
.tp-device-mobile { right: 0; width: min(8.5rem, 42vw); }
|
||||
.tp-earn-rate dl div { min-width: 0; }
|
||||
.tp-earn-process,.tp-calculation-grid,.tp-safety-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
.tp-legal-document article { line-height: 1.7; }
|
||||
.tp-legal-section { scroll-margin-top: 1rem; }
|
||||
@media print {
|
||||
.tp-legal-document nav, .tp-legal-document .btn, .tp-topbar { display: none !important; }
|
||||
.tp-legal-document article { width: 100%; max-width: none; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
document.querySelector('[data-token-toggle]')?.addEventListener('click', (event) => {
|
||||
const input = document.querySelector('#newApiKeyValue');
|
||||
const showing = input.type === 'text';
|
||||
input.type = showing ? 'password' : 'text';
|
||||
event.currentTarget.textContent = showing ? event.currentTarget.dataset.show : event.currentTarget.dataset.hide;
|
||||
});
|
||||
|
||||
document.querySelector('[data-token-copy]')?.addEventListener('click', async (event) => {
|
||||
const value = document.querySelector('#newApiKeyValue')?.value;
|
||||
if (!value) return;
|
||||
await navigator.clipboard.writeText(value);
|
||||
event.currentTarget.textContent = event.currentTarget.dataset.copied;
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-status-switch]').forEach((button) => button.closest('form').addEventListener('submit', () => {
|
||||
button.disabled = true;
|
||||
}));
|
||||
|
||||
document.querySelectorAll('[data-action-key]').forEach((button) => button.addEventListener('click', () => {
|
||||
const form = document.querySelector('[data-action-form]');
|
||||
const message = document.querySelector('[data-action-message]');
|
||||
const submit = document.querySelector('[data-action-submit]');
|
||||
form.action = button.dataset.url;
|
||||
document.querySelector('[data-action-method]').innerHTML = button.dataset.kind === 'delete' ? '<input type="hidden" name="_method" value="DELETE">' : '';
|
||||
message.textContent = (button.dataset.kind === 'delete' ? message.dataset.deleteTemplate : message.dataset.revokeTemplate).replace(':name', button.dataset.name);
|
||||
submit.textContent = button.textContent.trim();
|
||||
document.querySelector('[data-action-title]').textContent = button.textContent.trim();
|
||||
}));
|
||||
@@ -0,0 +1,72 @@
|
||||
import './bootstrap';
|
||||
import 'bootstrap';
|
||||
import './api-keys';
|
||||
import '../css/api-keys.css';
|
||||
import '../css/api-key-status.css';
|
||||
|
||||
const icons = { light: '☀', dark: '☾', system: '◐' };
|
||||
|
||||
function updateThemeControls(event) {
|
||||
const state = event?.detail ?? {
|
||||
preference: window.ThemeManager.getPreference(),
|
||||
resolved: window.ThemeManager.getResolvedTheme(),
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-theme-value]').forEach((button) => {
|
||||
const active = button.dataset.themeValue === state.preference;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-pressed', String(active));
|
||||
button.setAttribute('aria-current', active ? 'true' : 'false');
|
||||
const check = button.querySelector('[data-theme-check]');
|
||||
if (check) check.hidden = !active;
|
||||
});
|
||||
document.querySelectorAll('[data-theme-current-label]').forEach((node) => {
|
||||
node.textContent = document.querySelector(`[data-theme-value="${state.preference}"]`)?.dataset.themeLabel ?? state.preference;
|
||||
});
|
||||
document.querySelectorAll('[data-theme-current-icon]').forEach((node) => {
|
||||
node.textContent = icons[state.preference];
|
||||
});
|
||||
document.querySelectorAll('[data-theme-preference]').forEach((node) => {
|
||||
node.textContent = document.querySelector(`[data-theme-value="${state.preference}"]`)?.dataset.themeLabel ?? state.preference;
|
||||
});
|
||||
document.querySelectorAll('[data-theme-resolved]').forEach((node) => {
|
||||
node.textContent = document.querySelector(`[data-theme-value="${state.resolved}"]`)?.dataset.themeLabel ?? state.resolved;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const button = event.target.closest('[data-theme-value]');
|
||||
if (button) {
|
||||
window.ThemeManager.setPreference(button.dataset.themeValue);
|
||||
}
|
||||
});
|
||||
document.querySelector('[data-settings-theme]')?.addEventListener('change', event => window.ThemeManager.setPreference(event.target.value));
|
||||
if (document.querySelector('input[name="reduced_motion"]:checked')) document.documentElement.dataset.reducedMotion = 'true';
|
||||
|
||||
window.addEventListener('trafficpumper:theme-change', updateThemeControls);
|
||||
updateThemeControls();
|
||||
|
||||
function renderRankingCharts() {
|
||||
document.querySelectorAll('[data-ranking-chart]').forEach((container) => {
|
||||
const series = JSON.parse(container.dataset.rankingChart || '[]');
|
||||
const colors = ['#6d5dfc', '#16a085', '#e45175', '#e6a23c', '#2d8cf0', '#8e44ad'];
|
||||
const width = 900, height = 320, pad = 38;
|
||||
const points = series.flatMap((item) => item.points);
|
||||
if (!points.length) return;
|
||||
const dates = points.map((point) => new Date(point.date).getTime());
|
||||
const minDate = Math.min(...dates), maxDate = Math.max(...dates);
|
||||
const x = (date) => pad + ((new Date(date).getTime() - minDate) / Math.max(1, maxDate - minDate)) * (width - pad * 2);
|
||||
const y = (rank) => pad + ((rank - 1) / 99) * (height - pad * 2);
|
||||
const grid = [1, 10, 20, 50, 100].map((rank) => `<line x1="${pad}" y1="${y(rank)}" x2="${width-pad}" y2="${y(rank)}"/><text x="4" y="${y(rank)+4}">${rank}</text>`).join('');
|
||||
const paths = series.map((item, index) => {
|
||||
const coordinates = item.points.map((point) => `${x(point.date)},${y(point.rank)}`).join(' ');
|
||||
const dots = item.points.map((point) => `<circle cx="${x(point.date)}" cy="${y(point.rank)}" r="4"><title>${item.keyword} · ${item.project} · #${point.rank} · ${new Date(point.date).toLocaleString()}</title></circle>`).join('');
|
||||
return `<g style="--series:${colors[index%colors.length]}"><polyline points="${coordinates}"/>${dots}</g>`;
|
||||
}).join('');
|
||||
const legend = series.map((item,index)=>`<span><i style="background:${colors[index%colors.length]}"></i>${item.keyword}</span>`).join('');
|
||||
container.innerHTML = `<svg viewBox="0 0 ${width} ${height}" aria-hidden="true"><g class="tp-chart-grid">${grid}</g>${paths}</svg><div class="tp-chart-legend">${legend}</div>`;
|
||||
});
|
||||
}
|
||||
renderRankingCharts();
|
||||
window.addEventListener('trafficpumper:theme-change', renderRankingCharts);
|
||||
function renderVisibilityCharts(){document.querySelectorAll('[data-visibility-chart]').forEach(container=>{const data=JSON.parse(container.dataset.visibilityChart||'{}');const all=[...(data.current||[]),...(data.previous||[])];if(!all.length)return;const w=900,h=300,p=35;const dates=all.map(x=>new Date(x.date).getTime()),min=Math.min(...dates),max=Math.max(...dates);const x=d=>p+(new Date(d).getTime()-min)/Math.max(1,max-min)*(w-p*2),y=v=>h-p-(v/100)*(h-p*2);const series=(points,color,dash='')=>`<polyline style="stroke:${color};stroke-dasharray:${dash}" points="${points.map(q=>`${x(q.date)},${y(q.score)}`).join(' ')}"/>`+points.map(q=>`<circle style="--series:${color}" cx="${x(q.date)}" cy="${y(q.score)}" r="4"><title>${q.date} · ${q.score}% · ${q.measured}</title></circle>`).join('');container.innerHTML=`<svg viewBox="0 0 ${w} ${h}"><g class="tp-chart-grid">${[0,25,50,75,100].map(v=>`<line x1="${p}" y1="${y(v)}" x2="${w-p}" y2="${y(v)}"/><text x="2" y="${y(v)+4}">${v}%</text>`).join('')}</g>${series(data.current||[],'#6d5dfc')}${series(data.previous||[],'#16a085','7 5')}</svg>`;});}renderVisibilityCharts();window.addEventListener('trafficpumper:theme-change',renderVisibilityCharts);
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
@@ -0,0 +1,56 @@
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
var STORAGE_KEY = 'trafficpumper.theme';
|
||||
var DEFAULT_PREFERENCE = 'system';
|
||||
var VALID_PREFERENCES = ['light', 'dark', 'system'];
|
||||
var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
var accountTheme = typeof document.querySelector === 'function' ? document.querySelector('meta[name="trafficpumper-account-theme"]')?.content : null;
|
||||
var preference = VALID_PREFERENCES.indexOf(accountTheme) !== -1 ? accountTheme : readPreference();
|
||||
try { window.localStorage.setItem(STORAGE_KEY, preference); } catch (error) {}
|
||||
if (typeof document.querySelector === 'function' && document.querySelector('meta[name="trafficpumper-reduced-motion"]')?.content === 'true') document.documentElement.dataset.reducedMotion = 'true';
|
||||
|
||||
function isValid(value) { return VALID_PREFERENCES.indexOf(value) !== -1; }
|
||||
function readPreference() {
|
||||
try {
|
||||
var stored = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (isValid(stored)) return stored;
|
||||
window.localStorage.setItem(STORAGE_KEY, DEFAULT_PREFERENCE);
|
||||
return DEFAULT_PREFERENCE;
|
||||
} catch (error) { return DEFAULT_PREFERENCE; }
|
||||
}
|
||||
function resolve(value) {
|
||||
return value === 'system' ? (mediaQuery.matches ? 'dark' : 'light') : value;
|
||||
}
|
||||
function apply() {
|
||||
var resolved = resolve(preference);
|
||||
document.documentElement.setAttribute('data-bs-theme', resolved);
|
||||
document.documentElement.style.colorScheme = resolved;
|
||||
window.dispatchEvent(new window.CustomEvent('trafficpumper:theme-change', {
|
||||
detail: { preference: preference, resolved: resolved }
|
||||
}));
|
||||
return resolved;
|
||||
}
|
||||
function setPreference(value) {
|
||||
if (!isValid(value)) { return false; }
|
||||
preference = value;
|
||||
try { window.localStorage.setItem(STORAGE_KEY, value); } catch (error) {}
|
||||
apply();
|
||||
return true;
|
||||
}
|
||||
function onSystemThemeChange() {
|
||||
if (preference === 'system') { apply(); }
|
||||
}
|
||||
if (typeof mediaQuery.addEventListener === 'function') {
|
||||
mediaQuery.addEventListener('change', onSystemThemeChange);
|
||||
} else if (typeof mediaQuery.addListener === 'function') {
|
||||
mediaQuery.addListener(onSystemThemeChange);
|
||||
}
|
||||
window.ThemeManager = Object.freeze({
|
||||
storageKey: STORAGE_KEY,
|
||||
getPreference: function () { return preference; },
|
||||
getResolvedTheme: function () { return resolve(preference); },
|
||||
setPreference: setPreference,
|
||||
apply: apply
|
||||
});
|
||||
apply();
|
||||
}(window, document));
|
||||
@@ -0,0 +1,2 @@
|
||||
<x-layouts.app :title="__('ui.forgot_password').' — TrafficPumper'"><x-auth-card :title="__('ui.forgot_password')" :description="__('ui.forgot_description')"><form method="POST" action="{{ route('password.email') }}">@csrf
|
||||
<div class="mb-3"><label class="form-label" for="email">{{ __('ui.email') }}</label><input class="form-control" id="email" name="email" type="email" value="{{ old('email') }}" required autofocus></div><button class="btn btn-primary w-100" type="submit">{{ __('ui.send_link') }}</button></form></x-auth-card></x-layouts.app>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-layouts.app :title="__('ui.sign_in').' — TrafficPumper'"><x-auth-card :title="__('ui.sign_in')" :description="__('ui.login_description')">
|
||||
<form method="POST" action="{{ route('login') }}">@csrf
|
||||
<div class="mb-3"><label class="form-label" for="email">{{ __('ui.email') }}</label><input class="form-control" id="email" name="email" type="email" value="{{ old('email') }}" required autofocus autocomplete="email"></div>
|
||||
<div class="mb-3"><label class="form-label" for="password">{{ __('ui.password') }}</label><input class="form-control" id="password" name="password" type="password" required autocomplete="current-password"></div>
|
||||
<div class="form-check mb-3"><input class="form-check-input" id="remember" name="remember" type="checkbox"><label class="form-check-label" for="remember">{{ __('ui.remember') }}</label></div>
|
||||
<button class="btn btn-primary w-100" type="submit">{{ __('ui.sign_in') }}</button></form>
|
||||
<div class="tp-auth-links d-flex flex-wrap justify-content-between gap-2 mt-3 small"><a href="{{ route('password.request') }}">{{ __('ui.forgot_password') }}</a><a href="{{ route('register') }}">{{ __('ui.register') }}</a></div>
|
||||
</x-auth-card></x-layouts.app>
|
||||
@@ -0,0 +1,6 @@
|
||||
<x-layouts.app :title="__('ui.register').' — TrafficPumper'"><x-auth-card :title="__('ui.create_account')" :description="__('ui.register_description')"><form method="POST" action="{{ route('register') }}">@csrf
|
||||
<div class="mb-3"><label class="form-label" for="name">{{ __('ui.full_name') }}</label><input class="form-control" id="name" name="name" value="{{ old('name') }}" required autofocus autocomplete="name"></div>
|
||||
<div class="mb-3"><label class="form-label" for="email">{{ __('ui.email') }}</label><input class="form-control" id="email" name="email" type="email" value="{{ old('email') }}" required autocomplete="email"></div>
|
||||
<div class="mb-3"><label class="form-label" for="password">{{ __('ui.password') }}</label><input class="form-control" id="password" name="password" type="password" required autocomplete="new-password"></div>
|
||||
<div class="mb-3"><label class="form-label" for="password_confirmation">{{ __('ui.password_confirmation') }}</label><input class="form-control" id="password_confirmation" name="password_confirmation" type="password" required autocomplete="new-password"></div>
|
||||
<button class="btn btn-primary w-100" type="submit">{{ __('ui.register') }}</button></form></x-auth-card></x-layouts.app>
|
||||
@@ -0,0 +1,4 @@
|
||||
<x-layouts.app :title="__('ui.reset_password').' — TrafficPumper'"><x-auth-card :title="__('ui.reset_password')"><form method="POST" action="{{ route('password.store') }}">@csrf<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
<div class="mb-3"><label class="form-label" for="email">{{ __('ui.email') }}</label><input class="form-control" id="email" name="email" type="email" value="{{ old('email', $request->email) }}" required></div>
|
||||
<div class="mb-3"><label class="form-label" for="password">{{ __('ui.new_password') }}</label><input class="form-control" id="password" name="password" type="password" required autocomplete="new-password"></div>
|
||||
<div class="mb-3"><label class="form-label" for="password_confirmation">{{ __('ui.password_confirmation') }}</label><input class="form-control" id="password_confirmation" name="password_confirmation" type="password" required autocomplete="new-password"></div><button class="btn btn-primary w-100" type="submit">{{ __('ui.renew_password') }}</button></form></x-auth-card></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('ui.verify_email').' — TrafficPumper'"><x-auth-card :title="__('ui.verify_heading')" :description="__('ui.verify_description')"><form method="POST" action="{{ route('verification.send') }}">@csrf<button class="btn btn-primary w-100" type="submit">{{ __('ui.resend_verification') }}</button></form></x-auth-card></x-layouts.app>
|
||||
@@ -0,0 +1,12 @@
|
||||
@props(['title', 'description' => null])
|
||||
<main class="container tp-auth-page py-4 py-md-5">
|
||||
<div class="row g-0 justify-content-center"><div class="col-12 col-md-8 col-lg-5 tp-auth-column">
|
||||
<div class="card border-0 shadow-sm tp-auth-card"><div class="card-body p-3 p-sm-4 p-md-5">
|
||||
<div class="d-flex justify-content-center mb-4"><x-brand-logo :href="route('home')" height="76px" class="tp-brand-logo-auth" /></div>
|
||||
<h1 class="h3">{{ $title }}</h1>
|
||||
@if($description)<p class="text-body-secondary">{{ $description }}</p>@endif
|
||||
@if($errors->any())<div class="alert alert-danger"><ul class="mb-0">@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>@endif
|
||||
{{ $slot }}
|
||||
</div></div>
|
||||
</div></div>
|
||||
</main>
|
||||
@@ -0,0 +1,4 @@
|
||||
<span class="tp-brand-logo {{ $class }}" style="--tp-logo-height: {{ $height }}">
|
||||
<img class="tp-brand-logo-light" src="{{ asset('images/branding/TrafficPumper-logo-koyu.png') }}" width="2172" height="724" alt="TrafficPumper" decoding="async" fetchpriority="high">
|
||||
<img class="tp-brand-logo-dark" src="{{ asset('images/branding/TrafficPumper-logo-acik.png') }}" width="2172" height="724" alt="TrafficPumper" decoding="async" fetchpriority="high">
|
||||
</span>
|
||||
@@ -0,0 +1,7 @@
|
||||
@props(['href' => null, 'height' => '44px', 'class' => ''])
|
||||
@php($content = view('components.brand-logo-images', compact('height', 'class')))
|
||||
@if($href)
|
||||
<a href="{{ $href }}" class="tp-brand-logo-link" aria-label="TrafficPumper">{!! $content !!}</a>
|
||||
@else
|
||||
{!! $content !!}
|
||||
@endif
|
||||
@@ -0,0 +1 @@
|
||||
<div class="tp-customer-shell"><x-customer-sidebar /><main {{ $attributes->class(['tp-customer-main']) }}>{{ $slot }}</main></div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<button class="btn btn-outline-secondary d-lg-none tp-sidebar-toggle" type="button" data-bs-toggle="offcanvas" data-bs-target="#customerSidebar" aria-label="{{__('customer.open_menu')}}">☰</button><aside class="offcanvas-lg offcanvas-start tp-customer-sidebar" tabindex="-1" id="customerSidebar"><div class="offcanvas-header tp-sidebar-closebar"><button type="button" class="btn-close btn-close-white d-lg-none" data-bs-dismiss="offcanvas"></button></div><div class="offcanvas-body"><nav class="tp-side-nav">
|
||||
<a class="tp-side-link {{request()->routeIs('dashboard')?'active':''}}" href="{{route('dashboard')}}"><span>⌂</span>{{__('customer.dashboard')}}</a>
|
||||
<div class="tp-nav-section"><button data-bs-toggle="collapse" data-bs-target="#rankTracking">{{__('customer.rank_tracking')}} <span>⌄</span></button><div class="collapse show" id="rankTracking"><a href="{{route('dashboard')}}">{{__('customer.overview')}}</a><a class="{{request()->routeIs('projects.*')?'active':''}}" href="{{route('projects.index')}}">{{__('customer.projects')}}</a><a class="{{request()->routeIs('keywords.*')?'active':''}}" href="{{route('keywords.index')}}">{{__('customer.keywords')}}</a><a class="{{request()->routeIs('rankings.*')?'active':''}}" href="{{route('rankings.index')}}">{{__('customer.ranking_history')}}</a><a class="{{request()->routeIs('serp-checks.*')?'active':''}}" href="{{route('serp-checks.index')}}">{{__('customer.serp_checks')}}</a></div></div>
|
||||
<div class="tp-nav-section"><button data-bs-toggle="collapse" data-bs-target="#reports">{{__('customer.reports')}} <span>⌄</span></button><div class="collapse {{request()->routeIs('reports.*','competitors.*')?'show':''}}" id="reports"><a class="{{request()->routeIs('reports.visibility')?'active':''}}" href="{{route('reports.visibility')}}">{{__('customer.visibility')}}</a><a class="{{request()->routeIs('reports.winners')?'active':''}}" href="{{route('reports.winners')}}">{{__('customer.winners')}}</a><a class="{{request()->routeIs('reports.losers')?'active':''}}" href="{{route('reports.losers')}}">{{__('customer.losers')}}</a><a class="{{request()->routeIs('reports.competitors','competitors.*')?'active':''}}" href="{{route('reports.competitors')}}">{{__('customer.competitors')}}</a></div></div>
|
||||
<div class="tp-nav-section"><button data-bs-toggle="collapse" data-bs-target="#account">{{__('customer.my_account')}} <span>⌄</span></button><div class="collapse {{request()->routeIs('profile.*','settings.*')?'show':''}}" id="account"><a class="{{request()->routeIs('profile.*')?'active':''}}" href="{{route('profile.show')}}">{{__('customer.profile')}}</a><a class="{{request()->routeIs('settings.*')?'active':''}}" href="{{route('settings.show')}}">{{__('customer.settings')}}</a></div></div>
|
||||
</nav></div></aside>
|
||||
@@ -0,0 +1,2 @@
|
||||
@props(['title','description'])
|
||||
<div class="tp-empty-state"><div aria-hidden="true">⌁</div><h3>{{ $title }}</h3><p>{{ $description }}</p></div>
|
||||
@@ -0,0 +1,22 @@
|
||||
@props(['title' => 'TrafficPumper'])
|
||||
@php($navigation = app(\App\Support\PortalNavigationContext::class)->resolve(request()))
|
||||
<!doctype html>
|
||||
<html lang="{{ app()->getLocale() }}" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
@if($navigation['kind'] === 'web')<meta name="trafficpumper-account-theme" content="{{ $navigation['user']->setting?->theme ?? 'system' }}"><meta name="trafficpumper-reduced-motion" content="{{ $navigation['user']->setting?->reduced_motion ? 'true' : 'false' }}">@endif
|
||||
<title>{{ $title }}</title>
|
||||
<script>{!! file_get_contents(resource_path('js/theme-manager.js')) !!}</script>
|
||||
@livewireStyles
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body>
|
||||
<x-public-topbar :context="$navigation" />
|
||||
@if (session('status'))<div class="container pt-3"><div class="alert alert-success">{{ session('status') }}</div></div>@endif
|
||||
{{ $slot }}
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
@php($activeLocale = app()->getLocale())
|
||||
<div class="dropdown tp-locale-picker">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle d-inline-flex align-items-center gap-1" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-label="{{ __('ui.language') }}">
|
||||
<span aria-hidden="true">🌐</span><span>{{ strtoupper($activeLocale) }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
|
||||
@foreach(['tr' => 'Türkçe', 'en' => 'English'] as $code => $label)
|
||||
<li><form method="POST" action="{{ route('locale.update') }}">@csrf
|
||||
<input type="hidden" name="return_to" value="{{ request()->getRequestUri() }}">
|
||||
<button name="locale" value="{{ $code }}" type="submit" class="dropdown-item d-flex align-items-center gap-2" @if($activeLocale === $code) aria-current="true" @endif>
|
||||
<span class="text-center" style="width:1rem" aria-hidden="true">{{ $activeLocale === $code ? '✓' : '' }}</span><span>{{ $label }}</span>
|
||||
</button>
|
||||
</form></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
@props(['label','value'=>null,'icon'=>'◎','description'=>null])
|
||||
@php($statusDescription=$description??(is_string($value)&&mb_strlen($value)>12?$value:null))
|
||||
<article class="tp-metric-card"><div class="tp-metric-icon" aria-hidden="true">{{ $icon }}</div><div class="tp-metric-content"><p class="tp-metric-label">{{ $label }}</p><strong class="tp-metric-value">{{ $statusDescription?'—':($value??'—') }}</strong>@if($statusDescription)<small class="tp-metric-description">{{ $statusDescription }}</small>@endif</div></article>
|
||||
@@ -0,0 +1,16 @@
|
||||
@props(['context'])
|
||||
@php($authenticated = $context['kind'] !== 'guest')
|
||||
@php($menuId = $authenticated ? 'tp-auth-mobile-menu' : 'tp-public-mobile-menu')
|
||||
<nav class="navbar tp-topbar border-bottom bg-body-tertiary"><div class="container-fluid py-2 gap-2"><div class="tp-public-topbar d-flex align-items-center w-100 gap-2">
|
||||
<x-brand-logo :href="$authenticated ? $context['panel_url'] : route('home')" height="44px" />
|
||||
<div class="tp-public-desktop-actions d-none d-lg-flex align-items-center gap-2 ms-auto"><a class="btn btn-link text-decoration-none" href="{{route('earn')}}">{{__('earn.navigation')}}</a>
|
||||
@if($authenticated)<a class="btn btn-link text-decoration-none" href="{{$context['panel_url']}}">{{$context['panel_label']}}</a><form method="POST" action="{{$context['logout_route']}}">@csrf<button class="btn btn-outline-secondary" type="submit">{{__('ui.sign_out')}}</button></form>
|
||||
@else<a class="btn btn-link text-decoration-none" href="{{route('earn.login')}}">{{__('earn_portal.login_title')}}</a><a class="btn btn-link text-decoration-none" href="{{route('login')}}">{{__('ui.sign_in')}}</a><a class="btn btn-primary" href="{{route('register')}}">{{__('ui.register')}}</a>@endif
|
||||
<x-locale-picker /><x-theme-picker /></div>
|
||||
<button class="navbar-toggler tp-public-menu-toggle d-lg-none ms-auto" type="button" data-bs-toggle="offcanvas" data-bs-target="#{{$menuId}}" aria-controls="{{$menuId}}" aria-expanded="false" aria-label="{{__('navigation.open_menu')}}"><span class="navbar-toggler-icon" aria-hidden="true"></span></button>
|
||||
</div></div></nav>
|
||||
<div class="offcanvas offcanvas-end tp-public-mobile-menu" tabindex="-1" id="{{$menuId}}" aria-labelledby="{{$menuId}}-label"><div class="offcanvas-header"><h2 class="offcanvas-title h5" id="{{$menuId}}-label">{{__('navigation.menu')}}</h2><button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="{{__('navigation.close_menu')}}"></button></div><div class="offcanvas-body d-flex flex-column gap-3">
|
||||
<a class="btn btn-link" href="{{route('earn')}}">{{__('earn.navigation')}}</a>
|
||||
@if($authenticated)<a class="btn btn-primary" href="{{$context['panel_url']}}">{{$context['panel_label']}}</a><form method="POST" action="{{$context['logout_route']}}">@csrf<button class="btn btn-outline-secondary w-100" type="submit">{{__('ui.sign_out')}}</button></form>
|
||||
@else<a class="btn btn-outline-primary" href="{{route('earn.login')}}">{{__('earn_portal.login_title')}}</a><a class="btn btn-outline-primary" href="{{route('login')}}">{{__('ui.sign_in')}}</a><a class="btn btn-primary" href="{{route('register')}}">{{__('ui.register')}}</a>@endif
|
||||
<div class="tp-public-mobile-preferences d-flex align-items-center gap-2 pt-2 border-top" aria-label="{{__('ui.preferences')}}"><x-locale-picker /><x-theme-picker /></div></div></div>
|
||||
@@ -0,0 +1,2 @@
|
||||
@props(['change'=>null])
|
||||
@if(is_null($change))<span class="badge text-bg-info">{{ __('customer.new') }}</span>@elseif($change>0)<span class="tp-rank-up" aria-label="{{ __('customer.improved_by',['count'=>$change]) }}">↑ {{ $change }}</span>@elseif($change<0)<span class="tp-rank-down" aria-label="{{ __('customer.declined_by',['count'=>abs($change)]) }}">↓ {{ abs($change) }}</span>@else<span class="text-body-secondary">— {{ __('customer.unchanged') }}</span>@endif
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="dropdown tp-theme-picker">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle d-flex align-items-center gap-2" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-label="{{ __('ui.theme') }}">
|
||||
<span data-theme-current-icon aria-hidden="true">◐</span><span data-theme-current-label>{{ __('ui.system') }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
|
||||
@foreach(['light' => ['☀', __('ui.light')], 'dark' => ['☾', __('ui.dark')], 'system' => ['◐', __('ui.system')]] as $value => [$icon, $label])
|
||||
<li><button class="dropdown-item d-flex align-items-center gap-2" type="button" data-theme-value="{{ $value }}" data-theme-label="{{ $label }}" data-theme-icon="{{ $icon }}"><span aria-hidden="true">{{ $icon }}</span><span>{{ $label }}</span><span class="ms-auto" data-theme-check hidden aria-hidden="true">✓</span></button></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-layouts.app :title="__('customer.dashboard').' — TrafficPumper'"><div class="tp-customer-shell"><x-customer-sidebar/><main class="tp-customer-main">
|
||||
<span class="visually-hidden">{{ __('ui.welcome',['name'=>auth()->user()->name]) }}</span>
|
||||
<header class="tp-page-header"><div><p>{{ __('customer.rank_tracking') }}</p><h1>{{ __('customer.dashboard') }}</h1><span>{{ __('customer.dashboard_intro') }}</span></div><form class="tp-dashboard-filters" method="GET"><label>{{ __('customer.project') }}<select class="form-select" name="project" onchange="this.form.submit()"><option value="">{{ __('customer.all_projects') }}</option>@foreach($projects as $project)<option value="{{ $project->ulid }}" @selected($selectedProject?->id===$project->id)>{{ $project->name }}</option>@endforeach</select></label><label>{{ __('customer.period') }}<select class="form-select" name="days" onchange="this.form.submit()">@foreach([7,30,90] as $d)<option value="{{$d}}" @selected($days===$d)>{{ __('customer.last_days',['count'=>$d]) }}</option>@endforeach</select></label></form></header>
|
||||
<section class="tp-metric-grid"><x-metric-card :label="__('customer.active_projects')" :value="$metrics['projects']" icon="▣"/><x-metric-card :label="__('customer.tracked_keywords')" :value="$metrics['keywords']" icon="⌕"/><x-metric-card :label="__('customer.top10')" :value="$metrics['top10']" icon="↗"/><x-metric-card :label="__('customer.average_rank')" :value="$metrics['average']" icon="◎"/><x-metric-card :label="__('customer.rising')" :value="$metrics['winners']" icon="↑"/><x-metric-card :label="__('customer.falling')" :value="$metrics['losers']" icon="↓"/></section>
|
||||
<section class="tp-panel tp-chart-panel"><div class="tp-panel-heading"><div><p>{{ __('customer.performance') }}</p><h2>{{ __('customer.ranking_performance') }}</h2></div></div>@if($rows->whereNotNull('current')->isEmpty())<x-empty-state :title="__('customer.no_rank_data')" :description="__('customer.after_first_check')"/>@else<div class="tp-rank-bars">@foreach($rows->whereNotNull('current')->take(10) as $row)<div title="{{ $row['keyword']->phrase }}: {{ $row['current'] }}"><span>{{ Str::limit($row['keyword']->phrase,20) }}</span><i style="--rank:{{ min($row['current'],100) }}"></i><strong>#{{ $row['current'] }}</strong></div>@endforeach</div>@endif</section>
|
||||
<section class="tp-panel"><div class="tp-panel-heading"><div><p>{{ __('customer.latest_data') }}</p><h2>{{ __('customer.keyword_summary') }}</h2></div></div><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{ __('customer.keyword') }}</th><th>{{ __('customer.project') }}</th><th>{{ __('customer.current_rank') }}</th><th>{{ __('customer.previous_rank') }}</th><th>{{ __('customer.change') }}</th><th>{{ __('customer.target') }}</th><th>{{ __('customer.last_check') }}</th></tr></thead><tbody>@forelse($rows as $row)<tr><td><strong>{{ $row['keyword']->phrase }}</strong></td><td>{{ $row['project']->name }}</td><td>{{ $row['current']?'#'.$row['current']:'—' }}</td><td>{{ $row['previous']?'#'.$row['previous']:'—' }}</td><td><x-rank-change :change="$row['change']"/></td><td>{{ $row['keyword']->search_engine->label() }} · {{ $row['keyword']->device->label() }}</td><td>{{ $row['checked_at']?->diffForHumans()??'—' }}</td></tr>@empty<tr><td colspan="7"><x-empty-state :title="__('customer.no_keywords')" :description="__('customer.add_keyword_hint')"/></td></tr>@endforelse</tbody></table></div></section>
|
||||
<div class="tp-two-columns"><section class="tp-panel"><div class="tp-panel-heading"><h2>{{ __('customer.top_winners') }}</h2></div>@forelse($winners as $row)<div class="tp-mover"><span><strong>{{ $row['keyword']->phrase }}</strong><small>{{ $row['project']->name }}</small></span><x-rank-change :change="$row['change']"/></div>@empty<x-empty-state :title="__('customer.no_movement')" :description="__('customer.after_two_checks')"/>@endforelse</section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{ __('customer.top_losers') }}</h2></div>@forelse($losers as $row)<div class="tp-mover"><span><strong>{{ $row['keyword']->phrase }}</strong><small>{{ $row['project']->name }}</small></span><x-rank-change :change="$row['change']"/></div>@empty<x-empty-state :title="__('customer.no_movement')" :description="__('customer.after_two_checks')"/>@endforelse</section></div>
|
||||
</main></div></x-layouts.app>
|
||||
@@ -0,0 +1,12 @@
|
||||
<x-layouts.app :title="__('earn_portal.login_title').' — TrafficPumper'">
|
||||
<x-auth-card :title="__('earn_portal.login_title')" :description="__('earn_portal.login_description')">
|
||||
<form method="POST" action="{{ route('earn.login.store') }}">@csrf
|
||||
<div class="mb-3"><label class="form-label" for="email">{{ __('ui.email') }}</label><input class="form-control" id="email" name="email" type="email" value="{{ old('email') }}" required autofocus autocomplete="email"></div>
|
||||
<div class="mb-3"><label class="form-label" for="password">{{ __('ui.password') }}</label><input class="form-control" id="password" name="password" type="password" required autocomplete="current-password"></div>
|
||||
<div class="form-check mb-3"><input class="form-check-input" id="remember" name="remember" type="checkbox"><label class="form-check-label" for="remember">{{ __('ui.remember') }}</label></div>
|
||||
<button class="btn btn-primary w-100" type="submit">{{ __('earn_portal.sign_in') }}</button>
|
||||
</form>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-2 mt-3 small"><a href="{{ route('earn.register') }}">{{ __('earn_portal.create_account') }}</a><a href="{{ route('home') }}">{{ __('earn_portal.back_home') }}</a></div>
|
||||
<div class="d-flex align-items-center justify-content-center gap-2 mt-4 pt-3 border-top"><x-locale-picker /><x-theme-picker /></div>
|
||||
</x-auth-card>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,15 @@
|
||||
<x-layouts.app :title="__('earn_portal.register_title').' — TrafficPumper'">
|
||||
<x-auth-card :title="__('earn_portal.register_title')" :description="__('earn_portal.register_description')">
|
||||
<form method="POST" action="{{ route('earn.register.store') }}">@csrf
|
||||
<div class="mb-3"><label class="form-label" for="name">{{ __('earn_portal.name') }}</label><input class="form-control" id="name" name="name" value="{{ old('name') }}" required autocomplete="name"></div>
|
||||
<div class="mb-3"><label class="form-label" for="email">{{ __('ui.email') }}</label><input class="form-control" id="email" name="email" type="email" value="{{ old('email') }}" required autocomplete="email"></div>
|
||||
<div class="mb-3"><label class="form-label" for="password">{{ __('ui.password') }}</label><input class="form-control" id="password" name="password" type="password" required autocomplete="new-password"></div>
|
||||
<div class="mb-3"><label class="form-label" for="password_confirmation">{{ __('earn_portal.password_confirmation') }}</label><input class="form-control" id="password_confirmation" name="password_confirmation" type="password" required autocomplete="new-password"></div>
|
||||
<div class="form-check mb-2"><input class="form-check-input @error('terms_accepted') is-invalid @enderror" id="terms_accepted" name="terms_accepted" type="checkbox" value="1" @checked(old('terms_accepted')) required><label class="form-check-label" for="terms_accepted">{!! __('earn_legal.form.terms', ['link' => '<a href="'.route('earn.terms').'" target="_blank" rel="noopener noreferrer">'.__('earn_legal.form.terms_link').'</a>']) !!}</label>@error('terms_accepted')<div class="invalid-feedback">{{ $message }}</div>@enderror</div>
|
||||
<div class="form-check mb-3"><input class="form-check-input @error('privacy_notice_acknowledged') is-invalid @enderror" id="privacy_notice_acknowledged" name="privacy_notice_acknowledged" type="checkbox" value="1" @checked(old('privacy_notice_acknowledged')) required><label class="form-check-label" for="privacy_notice_acknowledged">{!! __('earn_legal.form.privacy', ['link' => '<a href="'.route('earn.privacy').'" target="_blank" rel="noopener noreferrer">'.__('earn_legal.form.privacy_link').'</a>']) !!}</label>@error('privacy_notice_acknowledged')<div class="invalid-feedback">{{ $message }}</div>@enderror</div>
|
||||
<button class="btn btn-primary w-100" type="submit">{{ __('earn_portal.register') }}</button>
|
||||
</form>
|
||||
<div class="d-flex flex-wrap justify-content-between gap-2 mt-3 small"><a href="{{ route('earn.login') }}">{{ __('earn_portal.have_account') }}</a><a href="{{ route('home') }}">{{ __('earn_portal.back_home') }}</a></div>
|
||||
<div class="d-flex align-items-center justify-content-center gap-2 mt-4 pt-3 border-top"><x-locale-picker /><x-theme-picker /></div>
|
||||
</x-auth-card>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,6 @@
|
||||
<x-layouts.app :title="__('earn_portal.verify_title').' — TrafficPumper'">
|
||||
<x-auth-card :title="__('earn_portal.verify_title')" :description="__('earn_portal.verify_description')">
|
||||
<form method="POST" action="{{ route('earn.verification.send') }}">@csrf<button class="btn btn-primary w-100">{{ __('earn_portal.resend_verification') }}</button></form>
|
||||
<form method="POST" action="{{ route('earn.logout') }}" class="mt-3">@csrf<button class="btn btn-outline-secondary w-100">{{ __('ui.sign_out') }}</button></form>
|
||||
</x-auth-card>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-layouts.app :title="$content['title'].' — TrafficPumper Earn'">
|
||||
<main class="container py-4 py-lg-5 tp-legal-document">
|
||||
<div class="row justify-content-center"><article class="col-12 col-lg-10 col-xl-9">
|
||||
<a href="{{ route('earn.register') }}" class="d-inline-block mb-3">← {{ __('earn_legal.back') }}</a>
|
||||
<header class="mb-4"><p class="text-primary fw-semibold mb-2">TRAFFICPUMPER EARN</p><h1 class="display-6">{{ $content['title'] }}</h1><div class="alert alert-warning"><strong>{{ __('earn_legal.draft') }}</strong><br>{{ __('earn_legal.version') }}: {{ $version }} · {{ __('earn_legal.effective_date') }}: {{ $effectiveDate }}</div><p class="lead">{{ $content['intro'] }}</p></header>
|
||||
<nav class="card mb-4" aria-label="{{ __('earn_legal.contents') }}"><div class="card-body"><h2 class="h5">{{ __('earn_legal.contents') }}</h2><ol class="row g-2 mb-0">@foreach($content['sections'] as $id => $section)<li class="col-12 col-md-6"><a href="#{{ $id }}">{{ $section['title'] }}</a></li>@endforeach</ol></div></nav>
|
||||
@foreach($content['sections'] as $id => $section)<section id="{{ $id }}" class="tp-legal-section py-3 border-bottom" tabindex="-1"><h2 class="h4"><a class="text-reset text-decoration-none" href="#{{ $id }}">{{ $section['title'] }}</a></h2>@foreach($section['paragraphs'] as $paragraph)<p>{{ $paragraph }}</p>@endforeach</section>@endforeach
|
||||
<p class="mt-4 text-body-secondary">{{ __('earn_legal.review_notice') }}</p>
|
||||
</article></div>
|
||||
</main>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,19 @@
|
||||
<x-layouts.app :title="__('earn_portal.api_keys').' — TrafficPumper'">
|
||||
<main class="container py-4 py-md-5 tp-api-keys">
|
||||
<header class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-4"><div><a href="{{route('earn.dashboard')}}" class="small">← {{__('earn_portal.dashboard')}}</a><h1 class="h2 mt-2 mb-1">{{__('earn_portal.api_keys')}}</h1><p class="text-body-secondary mb-0">{{__('earn_portal.api_keys_description')}}</p><p class="small text-body-secondary mt-2 mb-0">{{__('earn_portal.key_usage',['used'=>$tokens->count(),'max'=>$maxTotal])}}</p></div><button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createApiKey" @disabled($tokens->count() >= $maxTotal)>{{__('earn_portal.new_key')}}</button></header>
|
||||
@error('api_key')<div class="alert alert-danger" role="alert">{{$message}}</div>@enderror
|
||||
@if($tokens->count() >= $maxTotal)<div class="alert alert-warning">{{__('earn_portal.total_limit_reached')}}</div>@endif
|
||||
@if(session('new_api_key')) @php($newRecord=$tokens->first())<section class="alert alert-success tp-api-key-created"><h2 class="h5">{{__('earn_portal.created_title')}}</h2><p>{{__('earn_portal.created_warning')}}</p><div class="input-group mb-3"><input id="newApiKeyValue" class="form-control font-monospace" type="password" readonly value="{{session('new_api_key')}}" aria-label="{{__('earn_portal.api_key')}}"><button class="btn btn-outline-secondary" type="button" data-token-toggle data-show="{{__('earn_portal.show')}}" data-hide="{{__('earn_portal.hide')}}">{{__('earn_portal.show')}}</button><button class="btn btn-outline-secondary" type="button" data-token-copy data-copied="{{__('earn_portal.copied')}}">{{__('earn_portal.copy')}}</button></div><dl class="tp-key-meta"><div><dt>{{__('earn_portal.device_name')}}</dt><dd>{{$newRecord?->name}}</dd></div><div><dt>{{__('earn_portal.ability')}}</dt><dd><code>tasks:read</code></dd></div><div><dt>{{__('earn_portal.expires_at')}}</dt><dd>{{$newRecord?->expires_at?->translatedFormat('j F Y H:i') ?? __('earn_portal.no_expiration')}}</dd></div></dl><a href="{{route('earn.api-keys.index')}}" class="btn btn-success">{{__('earn_portal.done')}}</a></section>@endif
|
||||
@if($tokens->isEmpty())<section class="card text-center tp-api-empty"><div class="card-body"><div class="tp-key-icon" aria-hidden="true">⌁</div><h2 class="h4">{{__('earn_portal.empty_title')}}</h2><p class="text-body-secondary">{{__('earn_portal.empty_description')}}</p><button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createApiKey">{{__('earn_portal.first_key')}}</button></div></section>
|
||||
@else<section class="card"><div class="card-body"><p class="small text-body-secondary">{{__('earn_portal.lifecycle_help')}}</p><div class="table-responsive"><table class="table align-middle mb-0 tp-api-table"><thead><tr><th>{{__('earn_portal.device_name')}}</th><th>{{__('earn_portal.key_preview')}}</th><th>{{__('earn_portal.device_binding')}}</th><th>{{__('earn_portal.device_id_preview')}}</th><th>{{__('earn_portal.first_connection')}}</th><th>{{__('earn_portal.last_seen')}}</th><th>{{__('earn_portal.first_ip')}}</th><th>{{__('earn_portal.last_ip')}}</th><th>{{__('earn_portal.expires_at')}}</th><th>{{__('earn_portal.status')}}</th><th>{{__('earn_portal.actions')}}</th></tr></thead><tbody>
|
||||
@foreach($tokens as $token) @php($state=$token->status()->value)<tr><td data-label="{{__('earn_portal.device_name')}}">{{$token->name}}</td><td data-label="{{__('earn_portal.key_preview')}}"><code>tpe_••••••••{{str($token->token_hint)->after('tpe_')}}</code></td><td data-label="{{__('earn_portal.device_binding')}}">{{__($token->device_id_hash ? ($token->device_mismatch_seen ? 'earn_portal.device_mismatch_seen' : 'earn_portal.device_bound') : 'earn_portal.device_unbound')}}</td><td data-label="{{__('earn_portal.device_id_preview')}}"><code>{{$token->device_id_preview ?? '—'}}</code></td><td data-label="{{__('earn_portal.first_connection')}}">{{$token->device_bound_at?->translatedFormat('j F Y H:i') ?? '—'}}</td><td data-label="{{__('earn_portal.last_seen')}}">{{$token->last_seen_at?->translatedFormat('j F Y H:i') ?? __('earn_portal.never_used')}}</td><td data-label="{{__('earn_portal.first_ip')}}">{{$token->first_ip ?? '—'}}</td><td data-label="{{__('earn_portal.last_ip')}}">{{$token->last_ip ?? '—'}}</td><td data-label="{{__('earn_portal.expires_at')}}">{{$token->expires_at?->translatedFormat('j F Y H:i') ?? __('earn_portal.no_expiration')}}</td><td data-label="{{__('earn_portal.status')}}">
|
||||
@if(in_array($state,['active','inactive']))<form method="POST" action="{{route('earn.api-keys.status',$token->id)}}" class="tp-status-switch">@csrf @method('PATCH')<input type="hidden" name="is_active" value="{{$state==='active'?'0':'1'}}"><span>{{__('earn_portal.status_'.$state)}}</span><button type="submit" class="form-check-input" role="switch" aria-checked="{{$state==='active'?'true':'false'}}" aria-label="{{__($state==='active'?'earn_portal.deactivate':'earn_portal.activate')}}" data-status-switch></button></form>@else<span class="badge text-bg-{{$state==='expired'?'warning':'secondary'}}">{{__('earn_portal.status_'.$state)}}</span>@endif</td>
|
||||
<td data-label="{{__('earn_portal.actions')}}"><div class="dropdown"><button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false" aria-label="{{__('earn_portal.actions')}}">⋮</button><ul class="dropdown-menu dropdown-menu-end">@if(in_array($state,['active','inactive']))<li><form method="POST" action="{{route('earn.api-keys.status',$token->id)}}">@csrf @method('PATCH')<input type="hidden" name="is_active" value="{{$state==='active'?'0':'1'}}"><button class="dropdown-item">{{__($state==='active'?'earn_portal.deactivate':'earn_portal.activate')}}</button></form></li><li><button class="dropdown-item text-warning" data-action-key data-kind="revoke" data-url="{{route('earn.api-keys.revoke',$token->id)}}" data-name="{{$token->name}}" data-bs-toggle="modal" data-bs-target="#keyActionModal">{{__('earn_portal.permanent_revoke')}}</button></li>@endif<li><button class="dropdown-item text-danger" data-action-key data-kind="delete" data-url="{{route('earn.api-keys.destroy',$token->id)}}" data-name="{{$token->name}}" data-bs-toggle="modal" data-bs-target="#keyActionModal">{{__('earn_portal.delete')}}</button></li></ul></div></td></tr>@endforeach
|
||||
</tbody></table></div></div></section>@endif
|
||||
<section class="card mt-4"><div class="card-body"><h2 class="h5">{{__('earn_portal.example_title')}}</h2><pre class="tp-code-example"><code>curl -X GET "{{$tasksUrl}}" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer {{__('earn_portal.placeholder_key')}}" \
|
||||
-H "X-Device-ID: 5c9f63d12166490bb6a6459af51e8b83"</code></pre></div></section></main>
|
||||
<div class="modal fade" id="createApiKey" tabindex="-1" aria-labelledby="createApiKeyTitle" aria-hidden="true"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><form method="POST" action="{{route('earn.api-keys.store')}}">@csrf<div class="modal-header"><h2 class="modal-title fs-5" id="createApiKeyTitle">{{__('earn_portal.new_key')}}</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{__('earn_portal.cancel')}}"></button></div><div class="modal-body"><label class="form-label" for="keyName">{{__('earn_portal.device_name')}}</label><input id="keyName" class="form-control" name="name" value="{{old('name')}}" minlength="3" maxlength="80" required autocomplete="off"><div class="form-text">{{__('earn_portal.device_help')}}</div><label class="form-label mt-3" for="duration">{{__('earn_portal.expiration')}}</label><select id="duration" class="form-select" name="duration" required>@foreach($durations as $duration)<option value="{{$duration}}" @selected(old('duration',$defaultDuration)===$duration)>{{__('earn_portal.duration_'.$duration)}}</option>@endforeach</select><p class="form-text">{{__('earn_portal.unlimited_warning')}}</p><p class="form-text mb-0">{{__('earn_portal.fixed_ability')}} <code>tasks:read</code></p></div><div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{{__('earn_portal.cancel')}}</button><button class="btn btn-primary">{{__('earn_portal.create_key')}}</button></div></form></div></div></div>
|
||||
<div class="modal fade" id="keyActionModal" tabindex="-1" aria-labelledby="keyActionTitle" aria-hidden="true"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><form method="POST" data-action-form>@csrf<div data-action-method></div><div class="modal-header"><h2 class="modal-title fs-5" id="keyActionTitle" data-action-title></h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{__('earn_portal.cancel')}}"></button></div><div class="modal-body"><p data-action-message data-revoke-template="{{__('earn_portal.revoke_confirmation',['name'=>':name'])}}" data-delete-template="{{__('earn_portal.delete_confirmation',['name'=>':name'])}}"></p></div><div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{{__('earn_portal.cancel')}}</button><button class="btn btn-danger" data-action-submit></button></div></form></div></div></div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,6 @@
|
||||
<x-layouts.app :title="__('earn_portal.dashboard').' — TrafficPumper'">
|
||||
<main class="container py-4 py-md-5">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4"><div><p class="text-primary fw-semibold mb-1">TRAFFICPUMPER EARN</p><h1 class="h2 mb-0">{{ __('earn_portal.dashboard') }}</h1></div><div class="d-flex gap-2"><a class="btn btn-primary" href="{{ route('earn.api-keys.index') }}">{{ __('earn_portal.api_keys') }}</a><form method="POST" action="{{ route('earn.logout') }}">@csrf<button class="btn btn-outline-secondary">{{ __('ui.sign_out') }}</button></form></div></div>
|
||||
<div class="row g-3"><div class="col-12 col-md-4"><section class="card h-100"><div class="card-body"><h2 class="h6">{{ __('earn_portal.account_status') }}</h2><p class="h4 text-success mb-0">{{ __('earn_portal.status_active') }}</p></div></section></div><div class="col-12 col-md-4"><section class="card h-100"><div class="card-body"><h2 class="h6">{{ __('earn_portal.api_access') }}</h2><p class="h4 mb-0">{{ __('earn_portal.access_ready') }}</p></div></section></div><div class="col-12 col-md-4"><section class="card h-100"><div class="card-body"><h2 class="h6">{{ __('earn_portal.active_keys') }}</h2><p class="display-6 mb-0">{{ $activeTokenCount }}</p></div></section></div></div>
|
||||
<div class="alert alert-info mt-4">{{ __('earn_portal.test_stage') }}</div>
|
||||
</main></x-layouts.app>
|
||||
@@ -0,0 +1,29 @@
|
||||
<x-layouts.app :title="__('earn.meta_title')">
|
||||
<main class="tp-earn">
|
||||
<section class="tp-earn-hero">
|
||||
<div class="container">
|
||||
<div class="tp-earn-hero-copy">
|
||||
<span class="tp-landing-eyebrow">{{ __('earn.eyebrow') }}</span>
|
||||
<h1>{{ __('earn.hero_title') }}</h1>
|
||||
<p>{{ __('earn.hero_description') }}</p>
|
||||
<div class="tp-landing-actions"><a class="btn btn-primary btn-lg" href="#availability">{{ __('earn.early_access') }}</a><a class="btn btn-outline-secondary btn-lg" href="#how-it-works">{{ __('earn.how_link') }}</a></div>
|
||||
<span class="tp-coming-soon">{{ __('earn.coming_soon') }}</span>
|
||||
</div>
|
||||
<div class="tp-device-scene" aria-label="{{ __('earn.device_illustration') }}">
|
||||
<div class="tp-device tp-device-desktop"><span></span><div><i></i><i></i><i></i><strong>{{ __('earn.measurement_session') }}</strong></div></div>
|
||||
<div class="tp-device tp-device-mobile"><span></span><div><i></i><i></i><strong>{{ __('earn.verified_task') }}</strong></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tp-earn-rate" aria-labelledby="rate-title"><div class="container"><div><span>{{ __('earn.rate_label') }}</span><h2 id="rate-title">{{ number_format($tasksPerUsd, 0, ',', '.') }} {{ __('earn.tasks') }} = 1 USD</h2><p>{{ __('earn.rate_explanation') }}</p></div><dl><div><dt>{{ __('earn.minimum_payout') }}</dt><dd>{{ $minimumPayoutUsd }} USD</dd></div><div><dt>{{ __('earn.required_total') }}</dt><dd>{{ number_format($minimumPayoutTasks, 0, ',', '.') }} {{ __('earn.tasks') }}</dd></div></dl></div></section>
|
||||
|
||||
<section class="tp-landing-section" id="how-it-works" aria-labelledby="process-title"><div class="container"><div class="tp-section-heading"><span>{{ __('earn.process_eyebrow') }}</span><h2 id="process-title">{{ __('earn.process_title') }}</h2></div><ol class="tp-earn-process">@foreach(range(1,6) as $step)<li><span>{{ $step }}</span><div><h3>{{ __('earn.steps.'.$step.'.title', ['tasks' => number_format($tasksPerUsd, 0, ',', '.'), 'usd' => $minimumPayoutUsd]) }}</h3><p>{{ __('earn.steps.'.$step.'.description') }}</p></div></li>@endforeach</ol></div></section>
|
||||
|
||||
<section class="tp-landing-section tp-earn-calculation" aria-labelledby="calculation-title"><div class="container"><div class="tp-section-heading"><span>{{ __('earn.calculation_eyebrow') }}</span><h2 id="calculation-title">{{ __('earn.calculation_title') }}</h2><p>{{ __('earn.calculation_description') }}</p></div><div class="tp-calculation-grid">@foreach([1,10,$minimumPayoutUsd] as $usd)<article @class(['tp-calculation-card','tp-payout-card'=>$usd===$minimumPayoutUsd])><strong>{{ number_format($tasksPerUsd * $usd, 0, ',', '.') }}</strong><span>{{ __('earn.tasks') }}</span><i aria-hidden="true">→</i><b>{{ $usd }} USD</b>@if($usd===$minimumPayoutUsd)<small>{{ __('earn.payout_eligible') }}</small>@endif</article>@endforeach</div><ul class="tp-earn-disclosures"><li>{{ __('earn.only_verified') }}</li><li>{{ __('earn.availability_not_guaranteed') }}</li><li>{{ __('earn.no_fixed_income') }}</li></ul></div></section>
|
||||
|
||||
<section class="tp-landing-section tp-earn-safety" aria-labelledby="safety-title"><div class="container"><div class="tp-section-heading"><span>{{ __('earn.safety_eyebrow') }}</span><h2 id="safety-title">{{ __('earn.safety_title') }}</h2><p>{{ __('earn.safety_description') }}</p></div><ul class="tp-safety-grid">@foreach(range(1,11) as $item)<li><span aria-hidden="true">✓</span>{{ __('earn.safety.'.$item) }}</li>@endforeach</ul></div></section>
|
||||
|
||||
<section class="tp-landing-cta" id="availability"><div class="container"><div><h2>{{ __('earn.cta_title') }}</h2><p>{{ __('earn.cta_description') }}</p></div><span class="tp-coming-soon tp-coming-soon-light">{{ __('earn.coming_soon') }}</span></div></section>
|
||||
</main>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,92 @@
|
||||
<style>
|
||||
:root { --trafficpumper-admin-topbar-height: 4rem; }
|
||||
.fi-body-has-topbar .fi-topbar { min-height: var(--trafficpumper-admin-topbar-height); padding-inline: 1rem; }
|
||||
.fi-body-has-topbar .fi-topbar-start { width: calc(var(--sidebar-width) - 1rem); flex: 0 0 calc(var(--sidebar-width) - 1rem); margin-inline-end: 0; }
|
||||
.fi-body-has-topbar .fi-topbar-start .fi-logo { height: 2.5rem; width: auto; max-width: min(15rem, 100%); margin-inline-start: 0; object-fit: contain; object-position: left center; }
|
||||
.fi-body-has-topbar .fi-sidebar-header-ctn { display: none; }
|
||||
.fi-body-has-topbar .fi-sidebar-nav { padding-top: 1.5rem; }
|
||||
.fi-body-has-topbar .fi-topbar-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
gap: .5rem;
|
||||
margin-block: 0;
|
||||
}
|
||||
.trafficpumper-admin-preferences {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: .5rem;
|
||||
margin-block: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tp-admin-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .375rem;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
padding-inline: .625rem;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
border-radius: .625rem;
|
||||
color: rgb(55 65 81);
|
||||
background: transparent;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
.tp-admin-control:hover { background: rgb(249 250 251); color: rgb(17 24 39); }
|
||||
.tp-admin-control:focus-visible { outline: 2px solid rgb(245 158 11); outline-offset: 2px; }
|
||||
.dark .tp-admin-control { border-color: rgb(55 65 81); color: rgb(209 213 219); }
|
||||
.dark .tp-admin-control:hover { background: rgb(31 41 55); color: rgb(255 255 255); }
|
||||
.tp-admin-control .fi-icon { width: 1.125rem; height: 1.125rem; }
|
||||
.tp-admin-control-chevron { width: .875rem !important; height: .875rem !important; }
|
||||
.tp-admin-theme-icon { width: 1.125rem; font-size: 1rem; text-align: center; }
|
||||
.tp-admin-user-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
gap: .25rem;
|
||||
height: 2.5rem;
|
||||
margin: 0;
|
||||
padding: .125rem .375rem .125rem .125rem;
|
||||
border-radius: .625rem;
|
||||
}
|
||||
.tp-admin-user-control:hover { background: rgb(249 250 251); }
|
||||
.dark .tp-admin-user-control:hover { background: rgb(31 41 55); }
|
||||
.tp-admin-user-control:focus-visible { outline: 2px solid rgb(245 158 11); outline-offset: 2px; }
|
||||
.tp-admin-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 9999px;
|
||||
background: rgb(245 158 11);
|
||||
color: rgb(17 24 39);
|
||||
font-size: .75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.tp-admin-user-chevron { width: .875rem; height: .875rem; color: rgb(107 114 128); }
|
||||
.tp-admin-user-name, .tp-admin-user-email { display: block; max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tp-admin-user-email { margin-top: .125rem; color: rgb(107 114 128); font-size: .75rem; font-weight: 400; }
|
||||
@media (max-width: 63.999rem) {
|
||||
.fi-body-has-topbar .fi-topbar { gap: .5rem; }
|
||||
.fi-body-has-topbar .fi-topbar-start { display: flex; width: auto; flex: 1 1 auto; margin: 0; min-width: 0; }
|
||||
.fi-body-has-topbar .fi-topbar-start .fi-logo { height: 2.25rem; max-width: min(12rem, 42vw); }
|
||||
.fi-body-has-topbar .fi-topbar-end { gap: .5rem; }
|
||||
}
|
||||
@media (max-width: 39.999rem) {
|
||||
.fi-body-has-topbar .fi-topbar { padding-inline: .75rem; }
|
||||
.fi-body-has-topbar .fi-topbar-start .fi-logo { height: 2rem; max-width: 32vw; }
|
||||
.trafficpumper-admin-preferences, .fi-body-has-topbar .fi-topbar-end { gap: .25rem; }
|
||||
.tp-admin-control { padding-inline: .5rem; }
|
||||
.tp-admin-control-label, .tp-admin-control-chevron { display: none; }
|
||||
.tp-admin-user-control { width: 2.5rem; padding: .125rem; }
|
||||
.tp-admin-user-chevron { display: none; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
@php($activeLocale = app()->getLocale())
|
||||
<x-filament::dropdown placement="bottom-end">
|
||||
<x-slot name="trigger">
|
||||
<button type="button" class="tp-admin-control" aria-label="{{ __('ui.language') }}" title="{{ __('ui.language') }}">
|
||||
<x-filament::icon icon="heroicon-o-language" class="fi-icon fi-size-lg" />
|
||||
<span class="tp-admin-control-label">{{ strtoupper($activeLocale) }}</span>
|
||||
<x-filament::icon icon="heroicon-m-chevron-down" class="tp-admin-control-chevron" />
|
||||
</button>
|
||||
</x-slot>
|
||||
<x-filament::dropdown.list>
|
||||
@foreach(['tr' => 'Türkçe', 'en' => 'English'] as $code => $label)
|
||||
<form method="POST" action="{{ route('locale.update') }}">@csrf
|
||||
<input type="hidden" name="return_to" value="{{ request()->getRequestUri() }}">
|
||||
<x-filament::dropdown.list.item type="submit" name="locale" :value="$code" :icon="$activeLocale === $code ? 'heroicon-o-check' : null">
|
||||
{{ $label }}
|
||||
</x-filament::dropdown.list.item>
|
||||
</form>
|
||||
@endforeach
|
||||
</x-filament::dropdown.list>
|
||||
</x-filament::dropdown>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<x-filament::section><x-slot name="heading">Aktivasyon</x-slot><p>{{ $summary['activation_mode'] }}</p><p>Kill switch: {{ $summary['kill_switch'] ? 'AKTİF' : 'Kapalı' }}</p><p>Circuit: {{ $summary['circuit_open'] ? 'Açık' : 'Kapalı' }}</p></x-filament::section>
|
||||
<x-filament::section><x-slot name="heading">Son 24 saat</x-slot><p>Toplam: {{ $summary['checks_24h'] }}</p><p>Tamamlanan: {{ $summary['completed_24h'] }}</p><p>Başarısız: {{ $summary['failed_24h'] }}</p><p>Başarı: %{{ $summary['success_percent'] }}</p></x-filament::section>
|
||||
<x-filament::section><x-slot name="heading">Kuyruk</x-slot><p>Provider bekleyen: {{ $summary['waiting_provider'] }}</p><p>Takılı: {{ $summary['stuck'] }}</p><p>Backlog: {{ $summary['queue_backlog'] }}</p><p>Failed jobs: {{ $summary['failed_jobs'] }}</p></x-filament::section>
|
||||
<x-filament::section><x-slot name="heading">Bütçe (mikro USD)</x-slot><p>Günlük: {{ $summary['budget']['daily_micros'] }}</p><p>Aylık: {{ $summary['budget']['monthly_micros'] }}</p></x-filament::section>
|
||||
<x-filament::section><x-slot name="heading">Ramp-up</x-slot><p>Başlangıç: {{ $summary['ramp_up']['started_at'] ?? 'Başlamadı' }}</p><p>Kullanım: {{ $summary['ramp_up']['used'] }}</p><p>Geçerli limit: {{ $summary['ramp_up']['limit'] ?? 'Sınırsız' }}</p></x-filament::section>
|
||||
<x-filament::section><x-slot name="heading">Provider health</x-slot><p>Son kontrol: {{ $summary['last_health_at'] ?? 'Yapılmadı' }}</p><p>Credential: {{ config('serp.dataforseo.login') && config('serp.dataforseo.password') ? 'configured' : 'not configured' }}</p><p>Son hata: {{ $summary['last_error_category'] ?? '—' }}</p></x-filament::section>
|
||||
</div>
|
||||
@if(auth()->user()->hasRole('super_admin'))<div class="flex gap-3"><x-filament::button color="danger" wire:click="emergencyStop">Emergency stop</x-filament::button><x-filament::button color="gray" wire:click="clearEmergencyStop">Stop kaldır</x-filament::button></div>@endif
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="trafficpumper-admin-preferences" role="group" aria-label="{{ __('ui.preferences') }}">
|
||||
@include('filament.locale-picker')
|
||||
@include('filament.theme-picker')
|
||||
@auth
|
||||
@include('filament.user-menu')
|
||||
@endauth
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script>{!! file_get_contents(resource_path('js/theme-manager.js')) !!}</script>
|
||||
<script>
|
||||
try { localStorage.setItem('theme', window.ThemeManager.getPreference()); } catch (error) {}
|
||||
</script>
|
||||
<script>
|
||||
window.addEventListener('theme-changed', (event) => {
|
||||
if (window.ThemeManager) window.ThemeManager.setPreference(event.detail);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<div
|
||||
x-data="{
|
||||
theme: localStorage.getItem('theme') || @js(filament()->getDefaultThemeMode()->value),
|
||||
labels: @js(['light' => __('ui.light'), 'dark' => __('ui.dark'), 'system' => __('ui.system')]),
|
||||
icons: { light: '☀', dark: '☾', system: '◐' },
|
||||
choose(value) {
|
||||
this.theme = value
|
||||
window.dispatchEvent(new CustomEvent('theme-changed', { detail: value }))
|
||||
},
|
||||
}"
|
||||
x-on:theme-changed.window="theme = $event.detail"
|
||||
>
|
||||
<x-filament::dropdown placement="bottom-end" teleport>
|
||||
<x-slot name="trigger">
|
||||
<button type="button" class="tp-admin-control" aria-label="{{ __('ui.theme') }}" title="{{ __('ui.theme') }}">
|
||||
<span class="tp-admin-theme-icon" aria-hidden="true" x-text="icons[theme] || icons.system">◐</span>
|
||||
<span class="tp-admin-control-label" x-text="labels[theme] || labels.system">{{ __('ui.system') }}</span>
|
||||
<x-filament::icon icon="heroicon-m-chevron-down" class="tp-admin-control-chevron" />
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-filament::dropdown.list>
|
||||
@foreach(['light' => ['heroicon-o-sun', __('ui.light')], 'dark' => ['heroicon-o-moon', __('ui.dark')], 'system' => ['heroicon-o-computer-desktop', __('ui.system')]] as $value => [$icon, $label])
|
||||
<button
|
||||
type="button"
|
||||
class="fi-dropdown-list-item"
|
||||
x-on:click="choose('{{ $value }}')"
|
||||
x-bind:aria-current="theme === '{{ $value }}' ? 'true' : null"
|
||||
>
|
||||
<x-filament::icon :icon="$icon" class="fi-icon fi-size-md" />
|
||||
<span class="fi-dropdown-list-item-label">{{ $label }}</span>
|
||||
<span class="ms-auto" x-show="theme === '{{ $value }}'" aria-hidden="true">✓</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</x-filament::dropdown.list>
|
||||
</x-filament::dropdown>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
@php
|
||||
$user = auth()->user();
|
||||
$initials = collect(preg_split('/\s+/u', trim($user->name)) ?: [])
|
||||
->filter()
|
||||
->take(2)
|
||||
->map(fn (string $part) => mb_strtoupper(mb_substr($part, 0, 1)))
|
||||
->implode('');
|
||||
$profileUrl = $user->can('update', $user)
|
||||
? \App\Filament\Resources\Users\UserResource::getUrl('edit', ['record' => $user])
|
||||
: \App\Filament\Resources\Users\UserResource::getUrl();
|
||||
@endphp
|
||||
|
||||
<x-filament::dropdown placement="bottom-end" teleport>
|
||||
<x-slot name="trigger">
|
||||
<button type="button" class="tp-admin-user-control" aria-label="{{ __('filament-panels::layout.actions.open_user_menu.label') }}">
|
||||
<span class="tp-admin-avatar" aria-hidden="true">{{ $initials }}</span>
|
||||
<x-filament::icon icon="heroicon-m-chevron-down" class="tp-admin-user-chevron" />
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-filament::dropdown.header icon="heroicon-o-user-circle">
|
||||
<span class="tp-admin-user-name">{{ $user->name }}</span>
|
||||
<span class="tp-admin-user-email">{{ $user->email }}</span>
|
||||
</x-filament::dropdown.header>
|
||||
|
||||
<x-filament::dropdown.list>
|
||||
<x-filament::dropdown.list.item :href="$profileUrl" tag="a" icon="heroicon-o-user-circle">
|
||||
{{ __('customer.profile') }}
|
||||
</x-filament::dropdown.list.item>
|
||||
<form method="POST" action="{{ filament()->getLogoutUrl() }}">
|
||||
@csrf
|
||||
<x-filament::dropdown.list.item type="submit" icon="heroicon-o-arrow-left-on-rectangle">
|
||||
{{ __('ui.sign_out') }}
|
||||
</x-filament::dropdown.list.item>
|
||||
</form>
|
||||
</x-filament::dropdown.list>
|
||||
</x-filament::dropdown>
|
||||
@@ -0,0 +1,3 @@
|
||||
@csrf @if(($method??'POST')!=='POST')@method($method)@endif
|
||||
@if($bulk)<div class="mb-3"><label class="form-label" for="project">{{__('keywords.fields.project')}}</label><select id="project" name="project" class="form-select @error('project') is-invalid @enderror" required>@foreach($projects as $p)<option value="{{$p->ulid}}" @selected(old('project',$project->ulid)===$p->ulid)>{{$p->name}}</option>@endforeach</select>@error('project')<div class="invalid-feedback">{{$message}}</div>@enderror</div><div class="mb-3"><label class="form-label" for="phrases">{{__('keywords.fields.phrases')}}</label><textarea id="phrases" name="phrases" rows="8" class="form-control @error('phrases') is-invalid @enderror" required placeholder="{{__('keywords.placeholder')}}">{{old('phrases')}}</textarea><div class="form-text">{{__('keywords.bulk_limit',['count'=>100])}}</div>@error('phrases')<div class="invalid-feedback">{{$message}}</div>@enderror</div>@else<div class="mb-3"><label class="form-label" for="phrase">{{__('keywords.fields.phrase')}}</label><input id="phrase" name="phrase" maxlength="190" required class="form-control @error('phrase') is-invalid @enderror" value="{{old('phrase',$keyword->phrase)}}">@error('phrase')<div class="invalid-feedback">{{$message}}</div>@enderror</div>@endif
|
||||
<div class="row g-3 mb-4">@if($bulk)<div class="col-md-3"><label class="form-label">{{__('keywords.fields.engine')}}</label><select name="search_engine" class="form-select"><option value="google">Google</option></select></div>@endif<div class="col-md-3"><label class="form-label">{{__('keywords.fields.country')}}</label><select name="country_code" class="form-select">@foreach($countries as $v=>$l)<option value="{{$v}}" @selected(old('country_code',$keyword->country_code??'TR')===$v)>{{$l}}</option>@endforeach</select></div><div class="col-md-3"><label class="form-label">{{__('keywords.fields.language')}}</label><select name="language_code" class="form-select">@foreach($languages as $v=>$l)<option value="{{$v}}" @selected(old('language_code',$keyword->language_code??'tr')===$v)>{{$l}}</option>@endforeach</select></div><div class="col-md-3"><label class="form-label">{{__('keywords.fields.device')}}</label><select name="device" class="form-select">@foreach(\App\Enums\KeywordDevice::cases() as $v)<option value="{{$v->value}}" @selected(old('device',$keyword->device->value??'desktop')===$v->value)>{{$v->label()}}</option>@endforeach</select></div>@unless($bulk)<div class="col-md-3"><label class="form-label" for="status">{{__('keywords.status')}}</label><select id="status" name="status" required class="form-select @error('status') is-invalid @enderror"><option value="active" @selected(old('status',$keyword->status->value)==='active')>{{__('states.active')}}</option><option value="inactive" @selected(old('status',$keyword->status->value)==='inactive')>{{__('states.inactive')}}</option></select>@error('status')<div class="invalid-feedback">{{$message}}</div>@enderror</div>@endunless</div><button class="btn btn-primary">{{$submit}}</button> <a class="btn btn-link" href="{{route('keywords.index',['project'=>$project->ulid])}}">{{__('keywords.cancel')}}</a>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('keywords.add').' — TrafficPumper'"><x-customer-shell><header class="tp-page-header"><div><p>{{__('keywords.title')}}</p><h1>{{__('keywords.add')}}</h1><span>{{__('keywords.bulk_help')}}</span></div></header><section class="tp-panel tp-form-panel"><form method="POST" action="{{route('keywords.store')}}">@include('keywords._form',['submit'=>__('keywords.create'),'bulk'=>true])</form></section></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('keywords.edit').' — TrafficPumper'"><x-customer-shell><header class="tp-page-header"><div><p>{{$project->name}}</p><h1>{{__('keywords.edit')}}</h1></div></header><section class="tp-panel tp-form-panel"><form method="POST" action="{{route('keywords.update',$keyword)}}">@include('keywords._form',['submit'=>__('keywords.save'),'method'=>'PUT','bulk'=>false])</form></section></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,12 @@
|
||||
<x-layouts.app :title="__('keywords.title').' — TrafficPumper'"><x-customer-shell>
|
||||
<header class="tp-page-header"><div><p>{{__('customer.rank_tracking')}}</p><h1>{{__('keywords.title')}}</h1><span>{{__('keywords.description')}}</span></div><a class="btn btn-primary" href="{{route('keywords.create',['project'=>$projectUlid])}}">+ {{__('keywords.add')}}</a></header>
|
||||
@if(session('status'))<div class="alert alert-success">{{session('status')}}</div>@endif
|
||||
<section class="tp-metric-grid tp-keyword-metrics"><x-metric-card :label="__('keywords.metrics.total')" :value="$summary['total']" icon="⌕"/><x-metric-card :label="__('keywords.metrics.active')" :value="$summary['active']" icon="●"/><x-metric-card :label="__('customer.top10')" :value="$summary['top10']" icon="↗"/><x-metric-card :label="__('customer.average_rank')" :value="$summary['average']" icon="◎"/><x-metric-card :label="__('customer.rising')" :value="$summary['winners']" icon="↑"/><x-metric-card :label="__('customer.falling')" :value="$summary['losers']" icon="↓"/></section>
|
||||
<section class="tp-panel"><form method="GET" class="tp-keyword-filters"><input class="form-control" name="q" value="{{request('q')}}" placeholder="{{__('keywords.search')}}"><select class="form-select" name="project"><option value="">{{__('customer.all_projects')}}</option>@foreach($projects as $p)<option value="{{$p->ulid}}" @selected($projectUlid===$p->ulid)>{{$p->name}}</option>@endforeach</select><select class="form-select" name="status"><option value="" @selected(!request()->filled('status'))>{{__('keywords.view_all')}}</option><option value="active" @selected(request('status')==='active')>{{__('states.active')}}</option><option value="inactive" @selected(request('status')==='inactive')>{{__('states.inactive')}}</option><option value="archived" @selected(request('status')==='archived')>{{__('states.archived')}}</option></select><select class="form-select" name="country"><option value="">{{__('keywords.fields.country')}}</option>@foreach($countries as $v=>$l)<option value="{{$v}}" @selected(request('country')===$v)>{{$l}}</option>@endforeach</select><select class="form-select" name="device"><option value="">{{__('keywords.fields.device')}}</option>@foreach(\App\Enums\KeywordDevice::cases() as $v)<option value="{{$v->value}}" @selected(request('device')===$v->value)>{{$v->label()}}</option>@endforeach</select><button class="btn btn-outline-primary">{{__('keywords.filter')}}</button></form></section>
|
||||
<section class="tp-panel"><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('keywords.keyword')}}</th><th>{{__('keywords.project')}}</th><th>{{__('keywords.current')}}</th><th>{{__('keywords.previous')}}</th><th>{{__('keywords.change')}}</th><th>{{__('keywords.target')}}</th><th>{{__('keywords.status')}}</th><th>{{__('keywords.last_check')}}</th><th>{{__('keywords.actions')}}</th></tr></thead><tbody>
|
||||
@forelse($keywords as $keyword)<tr><td><a href="{{route('keywords.show',$keyword)}}">{{$keyword->phrase}}</a></td><td>{{$keyword->project->name}}</td><td>{{$keyword->current_rank?'#'.$keyword->current_rank:'—'}}</td><td>{{$keyword->previous_rank?'#'.$keyword->previous_rank:'—'}}</td><td>@if($keyword->movement==='winner')<span class="tp-rank-up" aria-label="{{__('keywords.winner')}}">↑ +{{$keyword->rank_change}}</span>@elseif($keyword->movement==='loser')<span class="tp-rank-down" aria-label="{{__('keywords.loser')}}">↓ {{$keyword->rank_change}}</span>@elseif($keyword->movement==='unchanged')<span>= {{__('customer.unchanged')}}</span>@else<span>{{__('customer.new')}}</span>@endif</td><td>{{$keyword->search_engine->label()}} · {{$keyword->country_code}}/{{$keyword->language_code}} · {{$keyword->device->label()}}</td>
|
||||
<td>@if($keyword->status===\App\Enums\KeywordStatus::Archived)<span class="badge text-bg-secondary">{{__('states.archived')}}</span>@else<form method="POST" action="{{route('keywords.status',$keyword)}}" class="d-inline">@csrf @method('PATCH')<input type="hidden" name="status" value="{{$keyword->status===\App\Enums\KeywordStatus::Active?'inactive':'active'}}"><button class="btn btn-sm {{$keyword->status===\App\Enums\KeywordStatus::Active?'btn-success':'btn-outline-secondary'}}" aria-label="{{__('keywords.status')}}: {{$keyword->status->label()}}">{{$keyword->status===\App\Enums\KeywordStatus::Active?'●':'○'}} {{$keyword->status->label()}}</button></form>@endif</td>
|
||||
<td>{{$keyword->last_checked_at?->diffForHumans()??__('keywords.not_checked')}}</td><td><div class="d-flex gap-1">@if($keyword->status!==\App\Enums\KeywordStatus::Archived)<a class="btn btn-sm btn-outline-secondary" href="{{route('keywords.edit',$keyword)}}">{{__('keywords.edit')}}</a><form method="POST" action="{{route('keywords.archive',$keyword)}}" onsubmit="return confirm('{{__('keywords.archive_confirm')}}')">@csrf<button class="btn btn-sm btn-outline-danger">{{__('keywords.archive')}}</button></form>@else<form method="POST" action="{{route('keywords.restore',$keyword)}}">@csrf<button class="btn btn-sm btn-outline-primary">{{__('keywords.restore')}}</button></form>@endif</div></td></tr>
|
||||
@empty<tr><td colspan="9"><x-empty-state :title="__('keywords.empty')" :description="__('keywords.empty_help')"/></td></tr>@endforelse
|
||||
</tbody></table></div><div class="mt-3">{{$keywords->links()}}</div></section>
|
||||
</x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,4 @@
|
||||
<x-layouts.app :title="$keyword->phrase.' — TrafficPumper'"><x-customer-shell><nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{{route('keywords.index',['project'=>$keyword->project->ulid])}}">{{__('keywords.title')}}</a></li><li class="breadcrumb-item active">{{$keyword->phrase}}</li></ol></nav><header class="tp-page-header"><div><p>{{$keyword->project->name}} · {{$keyword->project->domain}}</p><h1>{{$keyword->phrase}}</h1><span>{{$keyword->status->label()}}</span></div>@if($keyword->status===\App\Enums\KeywordStatus::Active)<a class="btn btn-outline-secondary" href="{{route('keywords.edit',$keyword)}}">{{__('keywords.edit')}}</a>@endif</header>
|
||||
<section class="tp-metric-grid tp-keyword-metrics"><x-metric-card :label="__('keywords.current')" :value="$keyword->current_rank?'#'.$keyword->current_rank:'—'" icon="◎"/><x-metric-card :label="__('keywords.previous')" :value="$keyword->previous_rank?'#'.$keyword->previous_rank:'—'" icon="◷"/><x-metric-card :label="__('keywords.change')" :value="$keyword->rank_change??__('customer.new')" icon="↕"/><x-metric-card :label="__('keywords.best')" :value="$best?'#'.$best:'—'" icon="★"/><x-metric-card :label="__('keywords.last_check')" :value="$keyword->last_checked_at?->diffForHumans()" icon="◴"/><x-metric-card :label="__('keywords.status')" :value="$keyword->status->label()" icon="●"/></section>
|
||||
<div class="tp-two-columns"><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('keywords.details')}}</h2></div><dl class="tp-detail-list"><div><dt>{{__('keywords.project')}}</dt><dd>{{$keyword->project->name}}</dd></div><div><dt>{{__('keywords.domain')}}</dt><dd>{{$keyword->project->domain}}</dd></div><div><dt>{{__('keywords.target')}}</dt><dd>{{$keyword->search_engine->label()}} · {{$countries[$keyword->country_code]}} · {{$languages[$keyword->language_code]}} · {{$keyword->device->label()}}</dd></div><div><dt>{{__('keywords.created')}}</dt><dd>{{$keyword->created_at->translatedFormat('d M Y H:i')}}</dd></div></dl></section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('keywords.history')}}</h2></div>@if($history->isEmpty())<x-empty-state :title="__('customer.no_rank_data')" :description="__('customer.after_first_check')"/>@else<div class="tp-history-list">@foreach($history as $point)<div><time>{{\Illuminate\Support\Carbon::parse($point['date'])->translatedFormat('d M Y')}}</time><span style="--rank:{{min($point['rank'],100)}}"></span><strong>#{{$point['rank']}}</strong></div>@endforeach</div>@endif</section></div>
|
||||
<section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('keywords.recent_checks')}}</h2></div><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('keywords.last_check')}}</th><th>{{__('keywords.current')}}</th><th>{{__('keywords.results')}}</th></tr></thead><tbody>@forelse($keyword->serpChecks->take(10) as $check)<tr><td>{{$check->checked_at?->translatedFormat('d M Y H:i')}}</td><td>{{$check->results->firstWhere('is_project_domain',true)?->position?'#'.$check->results->firstWhere('is_project_domain',true)->position:'100+'}}</td><td>{{$check->result_count}}</td></tr>@empty<tr><td colspan="3">{{__('keywords.not_checked')}}</td></tr>@endforelse</tbody></table></div></section></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,7 @@
|
||||
<x-layouts.app :title="__('profile.title')"><x-customer-shell><header class="tp-page-header"><div><p>{{__('customer.my_account')}}</p><h1>{{__('profile.title')}}</h1><span>{{__('profile.description')}}</span></div></header><div class="tp-profile-grid">
|
||||
<section class="tp-panel"><h2 class="h5">{{__('profile.information')}}</h2><form method="post" action="{{route('profile.update')}}">@csrf @method('PUT')<label class="form-label">{{__('profile.full_name')}}<input class="form-control" name="name" required maxlength="150" value="{{old('name',$u->name)}}"></label>@error('name')<p class="text-danger">{{$message}}</p>@enderror<button class="btn btn-primary">{{__('profile.save')}}</button></form></section>
|
||||
<section class="tp-panel"><h2 class="h5">{{__('profile.email_section')}}</h2><span class="badge {{$u->hasVerifiedEmail()?'text-bg-success':'text-bg-warning'}}">{{$u->hasVerifiedEmail()?__('profile.verified'):__('profile.not_verified')}}</span><form method="post" action="{{route('profile.email')}}" class="mt-3">@csrf @method('PUT')<label class="form-label">{{__('profile.email')}}<input class="form-control" type="email" name="email" required value="{{old('email',$u->email)}}"></label><label class="form-label">{{__('profile.current_password')}}<input class="form-control" type="password" name="current_password" required autocomplete="current-password"></label>@error('email')<p class="text-danger">{{$message}}</p>@enderror @error('current_password')<p class="text-danger">{{$message}}</p>@enderror<button class="btn btn-primary">{{__('profile.save')}}</button></form>@unless($u->hasVerifiedEmail())<form method="post" action="{{route('profile.verification.send')}}" class="mt-2">@csrf<button class="btn btn-outline-secondary">{{__('profile.resend')}}</button></form>@endunless</section>
|
||||
<section class="tp-panel"><h2 class="h5">{{__('profile.password_section')}}</h2><form method="post" action="{{route('profile.password')}}">@csrf @method('PUT')<label class="form-label">{{__('profile.current_password')}}<input class="form-control" type="password" name="current_password" required></label><label class="form-label">{{__('profile.new_password')}}<input class="form-control" type="password" name="password" required></label><label class="form-label">{{__('profile.confirm_password')}}<input class="form-control" type="password" name="password_confirmation" required></label>@error('password')<p class="text-danger">{{$message}}</p>@enderror<button class="btn btn-primary">{{__('profile.password_section')}}</button></form></section>
|
||||
<section class="tp-panel"><h2 class="h5">{{__('profile.roles_status')}}</h2><p>{{__('profile.roles')}}</p><div class="d-flex flex-wrap gap-2">@foreach($u->roles as $role)<span class="badge text-bg-secondary">{{__('roles.'.$role->name)}}</span>@endforeach</div><p class="mt-3">{{__('profile.status')}}: <strong>{{__('profile.'.($u->isActive()?'active':'inactive'))}}</strong></p>@if($u->canAccessPanel(filament()->getPanel('admin')))<a class="btn btn-outline-primary" href="/admin">{{__('profile.admin')}}</a>@endif</section>
|
||||
<section class="tp-panel tp-profile-sessions"><h2 class="h5">{{__('profile.sessions')}}</h2>@foreach($sessions as $session)<div class="tp-session-row"><span>{{$session['agent']?:'—'}}</span><small>{{$session['last_activity']}} @if($session['current'])· {{__('profile.current_device')}}@endif</small></div>@endforeach<form method="post" action="{{route('profile.sessions.logout-others')}}" class="mt-3">@csrf<label class="form-label">{{__('profile.current_password')}}<input class="form-control" type="password" name="current_password" required></label><button class="btn btn-outline-danger">{{__('profile.logout_others')}}</button></form></section>
|
||||
</div></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,4 @@
|
||||
@csrf @if(($method??'POST')!=='POST') @method($method) @endif
|
||||
<div class="mb-3"><label class="form-label" for="name">{{__('projects.project_name')}}</label><input class="form-control @error('name') is-invalid @enderror" id="name" name="name" maxlength="150" required value="{{old('name',$project->name??'')}}">@error('name')<div class="invalid-feedback">{{$message}}</div>@enderror</div>
|
||||
<div class="mb-4"><label class="form-label" for="website_url">{{__('projects.website_url')}}</label><input class="form-control @error('website_url') is-invalid @enderror" id="website_url" name="website_url" maxlength="2048" required placeholder="https://example.com" value="{{old('website_url',$project->url??'')}}"><div class="form-text">{{__('projects.url_help')}}</div>@error('website_url')<div class="invalid-feedback">{{$message}}</div>@enderror</div>
|
||||
<button class="btn btn-primary" type="submit">{{$submit}}</button><a class="btn btn-link" href="{{route('projects.index')}}">{{__('projects.cancel')}}</a>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('projects.new').' — TrafficPumper'"><x-customer-shell><nav aria-label="{{__('projects.breadcrumb')}}"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{{route('projects.index')}}">{{__('customer.projects')}}</a></li><li class="breadcrumb-item active">{{__('projects.new')}}</li></ol></nav><header class="tp-page-header"><div><p>{{__('customer.rank_tracking')}}</p><h1>{{__('projects.new')}}</h1></div></header><section class="tp-panel tp-form-panel"><form method="POST" action="{{route('projects.store')}}">@include('projects._form',['submit'=>__('projects.create')])</form></section></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('projects.edit').' — TrafficPumper'"><x-customer-shell><nav aria-label="{{__('projects.breadcrumb')}}"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{{route('projects.index')}}">{{__('customer.projects')}}</a></li><li class="breadcrumb-item"><a href="{{route('projects.show',$project)}}">{{$project->name}}</a></li><li class="breadcrumb-item active">{{__('projects.edit')}}</li></ol></nav><header class="tp-page-header"><div><p>{{__('customer.rank_tracking')}}</p><h1>{{__('projects.edit')}}</h1></div></header><section class="tp-panel tp-form-panel"><form method="POST" action="{{route('projects.update',$project)}}">@include('projects._form',['submit'=>__('projects.save'),'method'=>'PUT'])</form></section></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,2 @@
|
||||
<x-layouts.app :title="__('customer.projects').' — TrafficPumper'"><div class="tp-customer-shell"><x-customer-sidebar/><main class="tp-customer-main"><header class="tp-page-header"><div><p>{{ __('customer.rank_tracking') }}</p><h1>{{ __('customer.projects') }}</h1><span>{{ __('projects.intro') }}</span></div><a class="btn btn-primary" href="{{ route('projects.create') }}">+ {{ __('projects.new') }}</a></header>
|
||||
<div class="tp-panel"><ul class="nav nav-pills mb-3"><li class="nav-item"><a class="nav-link {{!$archived?'active':''}}" href="{{route('projects.index')}}">{{__('states.active')}}</a></li><li class="nav-item"><a class="nav-link {{$archived?'active':''}}" href="{{route('projects.index',['filter'=>'archived'])}}">{{__('states.archived')}}</a></li></ul><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('projects.name')}}</th><th>{{__('projects.domain')}}</th><th>{{__('customer.tracked_keywords')}}</th><th>{{__('roles.fields.status')}}</th><th class="text-end">{{__('projects.actions')}}</th></tr></thead><tbody>@forelse($projects as $project)<tr><td><a class="fw-semibold" href="{{route('projects.show',$project)}}">{{$project->name}}</a></td><td>{{$project->domain}}</td><td>{{$project->keywords_count}}</td><td><span class="badge text-bg-{{$archived?'secondary':'success'}}">{{$project->status->label()}}</span></td><td class="text-end">@if($archived)<form class="d-inline" method="POST" action="{{route('projects.restore',$project)}}">@csrf<button class="btn btn-sm btn-outline-primary">{{__('projects.restore')}}</button></form>@else<a class="btn btn-sm btn-outline-secondary" href="{{route('projects.edit',$project)}}">{{__('projects.edit')}}</a> <form class="d-inline" method="POST" action="{{route('projects.archive',$project)}}" onsubmit="return confirm('{{__('projects.archive_confirm')}}')">@csrf<button class="btn btn-sm btn-outline-danger">{{__('projects.archive')}}</button></form>@endif</td></tr>@empty<tr><td colspan="5"><x-empty-state :title="__('projects.empty')" :description="__('projects.empty_hint')"/></td></tr>@endforelse</tbody></table></div></div><div>{{$projects->links()}}</div></main></div></x-layouts.app>
|
||||
@@ -0,0 +1,7 @@
|
||||
<x-layouts.app :title="$project->name.' — TrafficPumper'"><x-customer-shell class="tp-project-detail">
|
||||
<nav aria-label="{{ __('projects.breadcrumb') }}"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{{ route('projects.index') }}">{{ __('customer.projects') }}</a></li><li class="breadcrumb-item active">{{ $project->name }}</li></ol></nav>
|
||||
<header class="tp-page-header"><div><p>{{ __('customer.rank_tracking') }}</p><h1>{{ $project->name }}</h1><span>{{ $project->domain }}</span></div><div class="d-flex flex-wrap gap-2"><span class="badge align-self-center text-bg-{{ $project->status===\App\Enums\ProjectStatus::Active?'success':'secondary' }}">{{ $project->status->label() }}</span>@if($project->status===\App\Enums\ProjectStatus::Active)<a class="btn btn-outline-secondary" href="{{route('projects.edit',$project)}}">{{__('projects.edit')}}</a><form method="POST" action="{{route('projects.archive',$project)}}" onsubmit="return confirm('{{__('projects.archive_confirm')}}')">@csrf<button class="btn btn-outline-danger">{{__('projects.archive')}}</button></form>@else<form method="POST" action="{{route('projects.restore',$project)}}">@csrf<button class="btn btn-primary">{{__('projects.restore')}}</button></form>@endif</div></header>
|
||||
<section class="tp-metric-grid tp-project-metrics"><x-metric-card :label="__('customer.tracked_keywords')" :value="$summary['keywords']" icon="⌕"/><x-metric-card :label="__('projects.last_serp_check')" :value="$summary['last_check']?->diffForHumans()" icon="◷"/><x-metric-card :label="__('customer.average_rank')" :value="$summary['average']" icon="◎"/><x-metric-card :label="__('customer.top10')" :value="$summary['top10']" icon="↗"/><x-metric-card :label="__('customer.rising')" :value="$summary['winners']" icon="↑"/><x-metric-card :label="__('customer.falling')" :value="$summary['losers']" icon="↓"/></section>
|
||||
<div class="tp-two-columns"><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('projects.details')}}</h2></div><dl class="tp-detail-list"><div><dt>{{__('projects.root_url')}}</dt><dd><a href="{{$project->url}}" rel="noopener noreferrer">{{$project->url}}</a></dd></div><div><dt>{{__('roles.fields.status')}}</dt><dd>{{$project->status->label()}}</dd></div><div><dt>{{__('projects.created')}}</dt><dd>{{$project->created_at->translatedFormat('d M Y H:i')}}</dd></div><div><dt>{{__('projects.updated')}}</dt><dd>{{$project->updated_at->translatedFormat('d M Y H:i')}}</dd></div><div class="tp-technical-detail"><dt>{{__('projects.id')}}</dt><dd><code>{{$project->ulid}}</code></dd></div></dl></section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('customer.ranking_performance')}}</h2></div>@if($summary['average']===null)<x-empty-state :title="__('customer.no_rank_data')" :description="__('customer.after_first_check')"/>@else<div class="tp-rank-bars"><div><span>{{__('customer.average_rank')}}</span><i style="--rank:{{min($summary['average'],100)}}"></i><strong>#{{$summary['average']}}</strong></div></div>@endif</section></div>
|
||||
<section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('customer.keywords')}}</h2><div class="d-flex gap-2"><a class="btn btn-sm btn-outline-secondary" href="{{route('keywords.index',['project'=>$project->ulid])}}">{{__('keywords.view_all')}}</a><a class="btn btn-sm btn-primary" href="{{route('keywords.create',['project'=>$project->ulid])}}">+ {{__('projects.add_keyword')}}</a></div></div>@if($project->keywords->isEmpty())<x-empty-state :title="__('customer.no_keywords')" :description="__('customer.add_keyword_hint')"/>@else<div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('customer.keyword')}}</th><th>{{__('customer.target')}}</th><th>{{__('roles.fields.status')}}</th></tr></thead><tbody>@foreach($project->keywords as $keyword)<tr><td><a href="{{route('keywords.show',$keyword)}}">{{$keyword->phrase}}</a></td><td>{{$keyword->search_engine->label()}} · {{$keyword->device->label()}}</td><td>{{$keyword->status->label()}}</td></tr>@endforeach</tbody></table></div>@endif</section>
|
||||
</x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('rankings.details').' — TrafficPumper'"><x-customer-shell><nav><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{{route('rankings.index')}}">{{__('rankings.title')}}</a></li><li class="breadcrumb-item"><a href="{{route('rankings.keywords.show',$check->keyword)}}">{{$check->keyword->phrase}}</a></li><li class="breadcrumb-item active">{{__('rankings.details')}}</li></ol></nav><header class="tp-page-header"><div><p>{{$check->keyword->project->name}}</p><h1>{{__('rankings.details')}}</h1><span>{{$check->checked_at?->translatedFormat('d M Y H:i')}}</span></div></header><div class="tp-two-columns"><section class="tp-panel"><dl class="tp-detail-list"><div><dt>{{__('rankings.keyword')}}</dt><dd>{{$check->keyword->phrase}}</dd></div><div><dt>{{__('rankings.project')}}</dt><dd>{{$check->keyword->project->name}}</dd></div><div><dt>{{__('rankings.rank')}}</dt><dd>{{$rank?'#'.$rank:'100+'}}</dd></div><div><dt>{{__('rankings.status')}}</dt><dd>{{$check->status->label()}}</dd></div><div><dt>{{__('rankings.country')}}</dt><dd>{{$check->country_code}} / {{$check->language_code}} / {{$check->device->label()}}</dd></div><div><dt>{{__('rankings.found_url')}}</dt><dd>{{$check->results->firstWhere('is_project_domain',true)?->url??'—'}}</dd></div></dl></section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('rankings.organic_results')}}</h2></div>@forelse($check->results as $result)<div class="tp-organic-result"><strong>#{{$result->position}} {{$result->title}}</strong><a href="{{$result->url}}" rel="noopener noreferrer">{{$result->display_url?:$result->url}}</a></div>@empty<x-empty-state :title="__('rankings.no_filter_data')" :description="__('rankings.no_checks')"/>@endforelse</section></div></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-layouts.app :title="__('rankings.title').' — TrafficPumper'"><x-customer-shell>
|
||||
<header class="tp-page-header"><div><p>{{__('customer.rank_tracking')}}</p><h1>{{__('rankings.title')}}</h1><span>{{__('rankings.description')}}</span></div></header>
|
||||
@if($errors->any())<div class="alert alert-danger">{{$errors->first()}}</div>@endif
|
||||
<section class="tp-panel"><form method="GET" class="tp-ranking-filters"><select name="project" class="form-select"><option value="">{{__('rankings.all_projects')}}</option>@foreach($projects as $project)<option value="{{$project->ulid}}" @selected(request('project')===$project->ulid)>{{$project->name}}</option>@endforeach</select><select name="keywords[]" class="form-select"><option value="">{{__('rankings.all_keywords')}}</option>@foreach($keywords as $keyword)<option value="{{$keyword->ulid}}" @selected(in_array($keyword->ulid,request('keywords',[])))>{{$keyword->phrase}}</option>@endforeach</select><select name="range" class="form-select">@foreach(['7d'=>'last7','30d'=>'last30','90d'=>'last90','this_month'=>'this_month','last_month'=>'last_month','custom'=>'custom'] as $value=>$label)<option value="{{$value}}" @selected(request('range','30d')===$value)>{{__('rankings.'.$label)}}</option>@endforeach</select><input type="date" name="from" value="{{request('from')}}" class="form-control" aria-label="{{__('rankings.from')}}"><input type="date" name="to" value="{{request('to')}}" class="form-control" aria-label="{{__('rankings.to')}}"><select name="movement" class="form-select"><option value="">{{__('rankings.movement')}}</option>@foreach(['winner','loser','unchanged','new','unranked'] as $v)<option value="{{$v}}" @selected(request('movement')===$v)>{{__('rankings.'.$v)}}</option>@endforeach</select><button class="btn btn-primary">{{__('rankings.filters')}}</button><a class="btn btn-outline-secondary" href="{{route('rankings.index')}}">{{__('rankings.clear')}}</a></form></section>
|
||||
<section class="tp-metric-grid tp-ranking-metrics"><x-metric-card :label="__('rankings.tracked')" :value="$summary['tracked']" icon="⌕"/><x-metric-card :label="__('rankings.average')" :value="$summary['average']" icon="◎"/><x-metric-card :label="__('rankings.top3')" :value="$summary['top3']" icon="★"/><x-metric-card :label="__('rankings.top10')" :value="$summary['top10']" icon="↗"/><x-metric-card :label="__('rankings.winners')" :value="$summary['winners']" icon="↑"/><x-metric-card :label="__('rankings.losers')" :value="$summary['losers']" icon="↓"/></section>
|
||||
<div class="tp-ranking-grid"><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('rankings.performance')}}</h2></div>@if($chart->isEmpty())<x-empty-state :title="__('rankings.no_checks')" :description="__('rankings.no_filter_data')"/>@else<div class="tp-ranking-chart" data-ranking-chart='@json($chart)' role="img" aria-label="{{__('rankings.performance')}}"></div><p class="small text-body-secondary mb-0">{{__('rankings.outside_help')}}</p>@endif</section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('rankings.distribution')}}</h2></div><div class="tp-distribution">@foreach($distribution as $key=>$count)<div><span>{{__('rankings.range.'.$key)}}</span><i style="--count:{{$count}}"></i><strong>{{$count}}</strong></div>@endforeach</div></section></div>
|
||||
<section class="tp-panel"><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('rankings.date')}}</th><th>{{__('rankings.keyword')}}</th><th>{{__('rankings.project')}}</th><th>{{__('rankings.rank')}}</th><th>{{__('rankings.previous')}}</th><th>{{__('rankings.change')}}</th><th>{{__('rankings.engine')}}</th><th>{{__('rankings.country')}}</th><th>{{__('rankings.language')}}</th><th>{{__('rankings.device')}}</th><th>{{__('rankings.status')}}</th><th>{{__('rankings.action')}}</th></tr></thead><tbody>@forelse($rows as $row)<tr><td>{{$row['check']->checked_at->translatedFormat('d M Y H:i')}}</td><td><a href="{{route('rankings.keywords.show',$row['keyword'])}}">{{$row['keyword']->phrase}}</a></td><td>{{$row['project']->name}}</td><td>{{$row['rank']?'#'.$row['rank']:'100+'}}</td><td>{{$row['previous']?'#'.$row['previous']:'—'}}</td><td><span class="tp-movement tp-{{$row['movement']}}">{{__('rankings.'.$row['movement'])}} @if($row['change']!==null){{$row['change']>0?'+':''}}{{$row['change']}}@endif</span></td><td>{{$row['check']->search_engine->label()}}</td><td>{{$row['check']->country_code}}</td><td>{{$row['check']->language_code}}</td><td>{{$row['check']->device->label()}}</td><td>{{$row['check']->status->label()}}</td><td><a href="{{route('rankings.checks.show',$row['check'])}}">{{__('rankings.view')}}</a></td></tr>@empty<tr><td colspan="12"><x-empty-state :title="__('rankings.no_filter_data')" :description="__('rankings.no_checks')"/></td></tr>@endforelse</tbody></table></div>{{$rows->links()}}</section>
|
||||
</x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="$keyword->phrase.' — '.__('rankings.title')"><x-customer-shell><nav><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{{route('rankings.index')}}">{{__('rankings.title')}}</a></li><li class="breadcrumb-item active">{{$keyword->phrase}}</li></ol></nav><header class="tp-page-header"><div><p>{{$keyword->project->name}} · {{$keyword->project->domain}}</p><h1>{{$keyword->phrase}}</h1><span>{{$keyword->search_engine->label()}} · {{$countries[$keyword->country_code]}} · {{$languages[$keyword->language_code]}} · {{$keyword->device->label()}}</span></div></header><section class="tp-metric-grid tp-ranking-metrics"><x-metric-card :label="__('rankings.current')" :value="$metrics['current']?'#'.$metrics['current']:'100+'" icon="◎"/><x-metric-card :label="__('rankings.previous')" :value="$metrics['previous']?'#'.$metrics['previous']:'—'" icon="◷"/><x-metric-card :label="__('rankings.best')" :value="$metrics['best']?'#'.$metrics['best']:'—'" icon="★"/><x-metric-card :label="__('rankings.worst')" :value="$metrics['worst']?'#'.$metrics['worst']:'—'" icon="↓"/><x-metric-card :label="__('rankings.measurements')" :value="$metrics['count']" icon="●"/><x-metric-card :label="__('rankings.top10_rate')" :value="$metrics['top10']!==null?$metrics['top10'].'%':null" icon="↗"/></section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('rankings.keyword_history')}}</h2></div><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('rankings.date')}}</th><th>{{__('rankings.rank')}}</th><th>{{__('rankings.previous')}}</th><th>{{__('rankings.change')}}</th><th>{{__('rankings.status')}}</th><th>{{__('rankings.action')}}</th></tr></thead><tbody>@forelse($rows as $row)<tr><td>{{$row['check']->checked_at->translatedFormat('d M Y H:i')}}</td><td>{{$row['rank']?'#'.$row['rank']:'100+'}}</td><td>{{$row['previous']?'#'.$row['previous']:'—'}}</td><td>{{__('rankings.'.$row['movement'])}}</td><td>{{$row['check']->status->label()}}</td><td><a href="{{route('rankings.checks.show',$row['check'])}}">{{__('rankings.view')}}</a></td></tr>@empty<tr><td colspan="6">{{__('rankings.no_checks')}}</td></tr>@endforelse</tbody></table></div></section></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="$c->name"><x-customer-shell><header class="tp-page-header"><div><p>{{__('competitors.title')}}</p><h1>{{$c->name}}</h1><span>{{$c->normalized_domain}} · {{$p->name}}</span></div></header><div class="tp-metric-grid"><x-metric-card :label="__('competitors.your_visibility')" :value="number_format($data['scores'][$p->domain]??0,2).'%'"/><x-metric-card :label="__('competitors.domain')" :value="number_format($data['scores'][$c->normalized_domain]??0,2).'%'"/><x-metric-card :label="__('competitors.lead')" :value="$data['lead']"/><x-metric-card :label="__('competitors.trail')" :value="$data['trail']"/></div></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,4 @@
|
||||
<x-layouts.app :title="__('competitors.title')"><x-customer-shell><header class="tp-page-header"><div><p>{{__('customer.reports')}}</p><h1>{{__('competitors.title')}}</h1><span>{{__('competitors.description')}}</span></div></header>
|
||||
@if($projects->isEmpty())<section class="tp-panel">{{__('competitors.empty_projects')}}</section>@else<section class="tp-panel mb-3"><form method="get" class="d-flex gap-2"><select name="project" class="form-select">@foreach($projects as $p)<option value="{{$p->ulid}}" @selected($project?->is($p))>{{$p->name}}</option>@endforeach</select><button class="btn btn-primary">{{__('losers.filter')}}</button></form></section>
|
||||
<section class="tp-panel mb-3"><h2 class="h5">{{__('competitors.add')}}</h2><form method="post" action="{{route('competitors.store',$project)}}" class="tp-competitor-form">@csrf<input class="form-control" name="name" placeholder="{{__('competitors.name')}}"><input class="form-control" name="domain" required placeholder="{{__('competitors.domain')}}"><button class="btn btn-primary">{{__('competitors.add')}}</button></form>@error('domain')<p class="text-danger">{{$message}}</p>@enderror<table class="table mt-3"><tbody>@forelse($project->competitors as $c)<tr><td>{{$c->name}}</td><td>{{$c->normalized_domain}}</td><td>{{__('competitors.'.$c->status)}}</td><td><a href="{{route('competitors.show',[$project,$c])}}">{{__('competitors.view')}}</a></td></tr>@empty<tr><td>{{__('competitors.empty_competitors')}}</td></tr>@endforelse</tbody></table></section>
|
||||
@if($data&&$competitors->isNotEmpty())@php($strong=$competitors->sortByDesc(fn($c)=>$data['scores'][$c->normalized_domain]??0)->first())<div class="tp-metric-grid mb-3"><x-metric-card :label="__('competitors.your_visibility')" :value="number_format($data['scores'][$project->domain]??0,2).'%'"/><x-metric-card :label="__('competitors.strongest')" :value="$strong?->name"/><x-metric-card :label="__('competitors.lead')" :value="$data['lead']"/><x-metric-card :label="__('competitors.trail')" :value="$data['trail']"/><x-metric-card :label="__('competitors.shared')" :value="$data['shared']"/><x-metric-card :label="__('competitors.unranked')" :value="$data['unranked']"/></div><section class="tp-panel"><div class="table-responsive"><table class="table"><thead><tr><th>{{__('competitors.keyword')}}</th><th>{{__('competitors.your_rank')}}</th><th>{{__('competitors.competitor_rank')}}</th><th>{{__('competitors.gap')}}</th><th>{{__('competitors.leader')}}</th></tr></thead><tbody>@forelse($data['rows'] as $row)<tr><td>{{$row['keyword']->phrase}}</td><td>{{$row['own']??'—'}}</td><td>{{$row['rival']??'—'}}</td><td>{{$row['gap']??'—'}}</td><td>{{__('competitors.'.($row['leader']==='you'?'you_lead':($row['leader']==='competitor'?'competitor_leads':($row['leader']==='tied'?'tied':'no_measurement'))))}}</td></tr>@empty<tr><td colspan="5">{{__('competitors.empty_data')}}</td></tr>@endforelse</tbody></table></div></section>@endif @endif</x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,17 @@
|
||||
<x-layouts.app :title="__('losers.title')"><x-customer-shell>
|
||||
<header class="tp-page-header"><div><p>{{__('customer.reports')}}</p><h1>{{__('losers.title')}}</h1><span>{{__('losers.description')}}</span></div></header>
|
||||
<section class="tp-panel mb-3"><form method="get" class="tp-losers-filters">
|
||||
<input class="form-control" name="q" value="{{request('q')}}" placeholder="{{__('losers.search')}}">
|
||||
<select class="form-select" name="project"><option value="">{{__('losers.all_projects')}}</option>@foreach($projects as $p)<option value="{{$p->ulid}}" @selected(request('project')===$p->ulid)>{{$p->name}}</option>@endforeach</select>
|
||||
<select class="form-select" name="range">@foreach(['7d'=>'last7','30d'=>'last30','90d'=>'last90','this_month'=>'this_month','last_month'=>'last_month','custom'=>'custom'] as $v=>$label)<option value="{{$v}}" @selected(request('range','30d')===$v)>{{__('visibility.'.$label)}}</option>@endforeach</select>
|
||||
<select class="form-select" name="country"><option value="">{{__('visibility.country')}}</option>@foreach($countries as $v=>$label)<option value="{{$v}}" @selected(request('country')===$v)>{{$label}}</option>@endforeach</select>
|
||||
<select class="form-select" name="language"><option value="">{{__('visibility.language')}}</option>@foreach($languages as $v=>$label)<option value="{{$v}}" @selected(request('language')===$v)>{{$label}}</option>@endforeach</select>
|
||||
<select class="form-select" name="device"><option value="">{{__('visibility.device')}}</option>@foreach(\App\Enums\KeywordDevice::cases() as $v)<option value="{{$v->value}}" @selected(request('device')===$v->value)>{{$v->label()}}</option>@endforeach</select>
|
||||
<select class="form-select" name="engine"><option value="">{{__('visibility.engine')}}</option>@foreach(\App\Enums\SearchEngine::cases() as $v)<option value="{{$v->value}}" @selected(request('engine')===$v->value)>{{$v->label()}}</option>@endforeach</select>
|
||||
<input class="form-control" type="number" min="1" max="100" name="min_lost" value="{{request('min_lost')}}" placeholder="{{__('losers.min_lost')}}"><input class="form-control" type="date" name="from" value="{{request('from')}}"><input class="form-control" type="date" name="to" value="{{request('to')}}"><button class="btn btn-primary">{{__('losers.filter')}}</button></form></section>
|
||||
<div class="tp-metric-grid tp-losers-metrics mb-3"><x-metric-card :label="__('losers.ranking_losers')" :value="$summary['losers']" icon="↘"/><x-metric-card :label="__('losers.total_lost')" :value="$summary['lost']" icon="−"/><x-metric-card :label="__('losers.top3')" :value="$summary['top3']" icon="3"/><x-metric-card :label="__('losers.top10')" :value="$summary['top10']" icon="10"/><x-metric-card :label="__('losers.top100')" :value="$summary['top100']" icon="100"/><x-metric-card :label="__('losers.visibility_loss')" :value="$summary['visibility_loss']===null?null:number_format($summary['visibility_loss'],2).'%'" :description="$summary['visibility_loss']===null?__('losers.no_comparison'):null" icon="%"/></div>
|
||||
@if($critical->isNotEmpty())<section class="tp-panel tp-critical-losses mb-3"><h2 class="h5">{{__('losers.critical')}}</h2>@foreach($critical as $row)<a class="badge text-bg-danger text-decoration-none me-2" href="{{route('rankings.keywords.show',$row['keyword']->ulid)}}">{{$row['keyword']->phrase}} · {{__('losers.categories.'.$row['category'])}}</a>@endforeach</section>@endif
|
||||
<section class="tp-panel mb-3"><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('losers.keyword')}}</th><th>{{__('losers.project')}}</th><th>{{__('losers.previous')}}</th><th>{{__('losers.current')}}</th><th>{{__('losers.lost')}}</th><th>{{__('losers.visibility_loss')}}</th><th>{{__('losers.category')}}</th><th>{{__('losers.target')}}</th><th>{{__('losers.device')}}</th><th>{{__('losers.last_check')}}</th><th></th></tr></thead><tbody>
|
||||
@forelse($rows as $row)<tr><td>{{$row['keyword']->phrase}}</td><td><a href="{{route('projects.show',$row['project']->ulid)}}">{{$row['project']->name}}</a></td><td>{{$row['previous']}}</td><td>{{$row['dropped_out']?__('losers.dropped_out_label'):$row['current']}}</td><td class="tp-loser">{{$row['lost']===null?'—':'−'.$row['lost']}}</td><td>{{number_format($row['visibility_loss'],2)}}%</td><td><span class="badge text-bg-danger">{{__('losers.categories.'.$row['category'])}}</span></td><td>{{$row['keyword']->country_code}} / {{$row['keyword']->language_code}}</td><td>{{$row['keyword']->device->label()}}</td><td>{{$row['checked_at']->format('d.m.Y H:i')}}</td><td><a href="{{route('rankings.keywords.show',$row['keyword']->ulid)}}">{{__('losers.view')}}</a></td></tr>@empty<tr><td colspan="11" class="text-center py-4 text-body-secondary">{{__('losers.empty')}}</td></tr>@endforelse</tbody></table></div>{{$rows->links()}}</section>
|
||||
<section class="tp-panel"><h2 class="h5">{{__('losers.distribution')}}</h2><div class="tp-distribution">@foreach($distribution as $key=>$count)<div><span>{{__('losers.ranges.'.$key)}}</span><i class="tp-loss-bar" style="--count:{{$count}}"></i><strong>{{$count}}</strong></div>@endforeach</div></section>
|
||||
</x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,14 @@
|
||||
<x-layouts.app :title="__('visibility.title').' — TrafficPumper'"><x-customer-shell>
|
||||
<header class="tp-page-header"><div><p>{{__('customer.reports')}}</p><h1>{{__('visibility.title')}}</h1><span>{{__('visibility.description')}}</span></div></header>
|
||||
<section class="tp-panel"><form method="GET" class="tp-visibility-filters">
|
||||
<select name="project" class="form-select"><option value="">{{__('visibility.all_projects')}}</option>@foreach($projects as $project)<option value="{{$project->ulid}}" @selected(request('project')===$project->ulid)>{{$project->name}}</option>@endforeach</select>
|
||||
<select name="range" class="form-select">@foreach(['7d'=>'last7','30d'=>'last30','90d'=>'last90','this_month'=>'this_month','last_month'=>'last_month','custom'=>'custom'] as $v=>$l)<option value="{{$v}}" @selected(request('range','30d')===$v)>{{__('visibility.'.$l)}}</option>@endforeach</select>
|
||||
<select name="compare" class="form-select"><option value="previous">{{__('visibility.compare_previous')}}</option><option value="none" @selected(request('compare')==='none')>{{__('visibility.no_compare')}}</option></select>
|
||||
<select name="country" class="form-select"><option value="">{{__('visibility.country')}}</option>@foreach($countries as $v=>$l)<option value="{{$v}}">{{$l}}</option>@endforeach</select>
|
||||
<select name="device" class="form-select"><option value="">{{__('visibility.device')}}</option>@foreach(\App\Enums\KeywordDevice::cases() as $v)<option value="{{$v->value}}">{{$v->label()}}</option>@endforeach</select>
|
||||
<input type="date" name="from" value="{{request('from')}}" class="form-control"><input type="date" name="to" value="{{request('to')}}" class="form-control"><button class="btn btn-primary">{{__('visibility.filter')}}</button><a class="btn btn-outline-secondary" href="{{route('reports.visibility')}}">{{__('visibility.clear')}}</a></form></section>
|
||||
<div class="alert alert-info"><strong>{{__('visibility.formula')}}</strong><br>{{__('visibility.coverage',['measured'=>$coverage[0],'total'=>$coverage[1]])}}@if($coverage[0]<$coverage[1])<br>{{__('visibility.coverage_warning')}}@endif</div>
|
||||
<section class="tp-metric-grid tp-visibility-metrics"><x-metric-card :label="__('visibility.score')" :value="$metrics['current']!==null?number_format($metrics['current'],2).'%':null" icon="◎"/><x-metric-card :label="__('visibility.previous')" :value="$metrics['previous']!==null?number_format($metrics['previous'],2).'%':__('visibility.no_comparison')" icon="◷"/><x-metric-card :label="__('visibility.change')" :value="$metrics['change']!==null?number_format($metrics['change'],2).'%':null" icon="↕"/><x-metric-card :label="__('visibility.measured')" :value="$metrics['measured']" icon="●"/><x-metric-card :label="__('visibility.top10')" :value="$metrics['top10']" icon="↗"/><x-metric-card :label="__('visibility.unranked')" :value="$metrics['unranked']" icon="—"/></section>
|
||||
<div class="tp-ranking-grid"><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('visibility.trend')}}</h2></div>@if($trend->isEmpty())<x-empty-state :title="__('visibility.no_data')" :description="__('visibility.formula')"/>@else<div class="tp-visibility-chart" data-visibility-chart='@json(['current'=>$trend,'previous'=>$previousTrend])'></div>@endif</section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('visibility.distribution')}}</h2></div><div class="tp-distribution">@foreach($distribution as $key=>$count)<div><span>{{__('visibility.range_labels.'.$key)}}</span><i style="--count:{{$count}}"></i><strong>{{$count}}</strong></div>@endforeach</div></section></div>
|
||||
<div class="tp-two-columns">@foreach(['winners'=>$winners,'losers'=>$losers] as $title=>$items)<section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('visibility.'.$title)}}</h2></div><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('visibility.keyword')}}</th><th>{{__('visibility.previous_rank')}}</th><th>{{__('visibility.current_rank')}}</th><th>{{__('visibility.point_change')}}</th></tr></thead><tbody>@forelse($items as $item)<tr><td><a href="{{route('rankings.keywords.show',$item['keyword'])}}">{{$item['keyword']->phrase}}</a><small class="d-block text-body-secondary">{{$item['project']->name}}</small></td><td>{{$item['previous']?'#'.$item['previous']:'100+'}}</td><td>{{$item['current']?'#'.$item['current']:'100+'}}</td><td class="{{$item['delta']>0?'tp-rank-up':'tp-rank-down'}}">{{$item['delta']>0?'+':''}}{{$item['delta']}}</td></tr>@empty<tr><td colspan="4">{{__('visibility.no_comparison')}}</td></tr>@endforelse</tbody></table></div></section>@endforeach</div>
|
||||
</x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,16 @@
|
||||
<x-layouts.app :title="__('winners.title')"><x-customer-shell>
|
||||
<div class="tp-page-heading"><div><h1>{{ __('winners.title') }}</h1><p>{{ __('winners.description') }}</p></div></div>
|
||||
<section class="tp-panel mb-3"><form method="get" action="{{ route('reports.winners') }}" class="tp-winners-filters">
|
||||
<label>{{ __('winners.search') }}<input class="form-control" name="q" value="{{ request('q') }}"></label>
|
||||
<label>{{ __('winners.project') }}<select class="form-select" name="project"><option value="">{{ __('winners.all_projects') }}</option>@foreach($projects as $project)<option value="{{ $project->ulid }}" @selected(request('project')===$project->ulid)>{{ $project->name }}</option>@endforeach</select></label>
|
||||
<label>{{ __('visibility.period') }}<select class="form-select" name="range">@foreach(['7d'=>'last7','30d'=>'last30','90d'=>'last90','this_month'=>'this_month','last_month'=>'last_month','custom'=>'custom'] as $range=>$label)<option value="{{ $range }}" @selected(request('range','30d')===$range)>{{ __('visibility.'.$label) }}</option>@endforeach</select></label>
|
||||
<label>{{ __('visibility.country') }}<select class="form-select" name="country"><option value="">—</option>@foreach($countries as $code=>$name)<option value="{{ $code }}" @selected(request('country')===$code)>{{ $name }}</option>@endforeach</select></label>
|
||||
<label>{{ __('visibility.language') }}<select class="form-select" name="language"><option value="">—</option>@foreach($languages as $code=>$name)<option value="{{ $code }}" @selected(request('language')===$code)>{{ $name }}</option>@endforeach</select></label>
|
||||
<label>{{ __('winners.min_gained') }}<input class="form-control" type="number" min="1" max="100" name="min_gained" value="{{ request('min_gained') }}"></label>
|
||||
<label>{{ __('visibility.from_date') }}<input class="form-control" type="date" name="from" value="{{ request('from') }}"></label><label>{{ __('visibility.to_date') }}<input class="form-control" type="date" name="to" value="{{ request('to') }}"></label>
|
||||
<div class="d-flex align-items-end"><button class="btn btn-primary w-100">{{ __('winners.filter') }}</button></div>
|
||||
</form></section>
|
||||
<div class="tp-metric-grid tp-winners-metrics mb-3"><x-metric-card :label="__('winners.ranking_winners')" :value="$summary['winners']" icon="↗"/><x-metric-card :label="__('winners.total_gained')" :value="$summary['gained']" icon="+"/><x-metric-card :label="__('winners.top3')" :value="$summary['top3']" icon="3"/><x-metric-card :label="__('winners.top10')" :value="$summary['top10']" icon="10"/><x-metric-card :label="__('winners.new_entries')" :value="$summary['new_entries']" icon="★"/><x-metric-card :label="__('winners.contribution')" :value="$summary['contribution']===null?null:number_format($summary['contribution'],2).'%'" :description="$summary['contribution']===null?__('winners.no_comparison'):null" icon="%"/></div>
|
||||
<section class="tp-panel mb-3"><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{ __('winners.keyword') }}</th><th>{{ __('winners.project') }}</th><th>{{ __('winners.previous') }}</th><th>{{ __('winners.current') }}</th><th>{{ __('winners.gained') }}</th><th>{{ __('winners.visibility') }}</th><th>{{ __('winners.last_check') }}</th></tr></thead><tbody>@forelse($rows as $row)<tr><td><a href="{{ route('rankings.keywords.show',$row['keyword']->ulid) }}">{{ $row['keyword']->phrase }}</a><br><small class="text-body-secondary">{{ __('winners.categories.'.$row['category']) }}</small></td><td><a href="{{ route('projects.show',$row['project']->ulid) }}">{{ $row['project']->name }}</a></td><td>{{ $row['previous']??'—' }}</td><td>{{ $row['current']??'—' }}</td><td class="tp-winner">{{ $row['new_entry']?__('winners.categories.new_entry'):'+'.$row['gained'] }}</td><td>{{ number_format($row['contribution'],2) }}%</td><td>{{ $row['checked_at']->format('d.m.Y H:i') }}</td></tr>@empty<tr><td colspan="7" class="text-center text-body-secondary py-4">{{ __('winners.empty') }}</td></tr>@endforelse</tbody></table></div>{{ $rows->links() }}</section>
|
||||
<section class="tp-panel"><h2 class="h5">{{ __('winners.distribution') }}</h2><div class="tp-distribution">@foreach($distribution as $key=>$count)<div><span>{{ __('winners.ranges.'.$key) }}</span><i style="--count:{{ $count }}"></i><strong>{{ $count }}</strong></div>@endforeach</div></section>
|
||||
</x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,4 @@
|
||||
<x-layouts.app :title="__('serp_checks.title').' — TrafficPumper'"><x-customer-shell><header class="tp-page-header"><div><p>{{__('customer.rank_tracking')}}</p><h1>{{__('serp_checks.title')}}</h1><span>{{__('serp_checks.description')}}</span></div></header>@error('serp')<div class="alert alert-warning">{{$message}}</div>@enderror @if(!config('serp.enabled')||app(\App\Serp\Operations\ActivationManager::class)->allowsNormal()===false)<div class="alert alert-secondary">{{__('serp_checks.unavailable')}}</div>@endif
|
||||
<section class="tp-metric-grid tp-serp-metrics"><x-metric-card :label="__('serp_checks.total')" :value="$summary['total']" icon="⌕"/><x-metric-card :label="__('serp_checks.pending')" :value="$summary['pending']" icon="◷"/><x-metric-card :label="__('serp_checks.running')" :value="$summary['running']" icon="↻"/><x-metric-card :label="__('serp_checks.completed')" :value="$summary['completed']" icon="✓"/><x-metric-card :label="__('serp_checks.failed')" :value="$summary['failed']" icon="!"/><x-metric-card :label="__('serp_checks.last24')" :value="$summary['last24']" icon="◴"/></section>
|
||||
<section class="tp-panel"><form method="GET" class="tp-serp-filters"><input name="q" class="form-control" value="{{request('q')}}" placeholder="{{__('serp_checks.search')}}"><select name="project" class="form-select"><option value="">{{__('serp_checks.all_projects')}}</option>@foreach($projects as $project)<option value="{{$project->ulid}}" @selected(request('project')===$project->ulid)>{{$project->name}}</option>@endforeach</select><select name="keyword" class="form-select"><option value="">{{__('serp_checks.all_keywords')}}</option>@foreach($keywords as $keyword)<option value="{{$keyword->ulid}}" @selected(request('keyword')===$keyword->ulid)>{{$keyword->phrase}}</option>@endforeach</select><select name="status" class="form-select"><option value="">{{__('serp_checks.all_statuses')}}</option>@foreach(\App\Enums\SerpCheckStatus::cases() as $status)<option value="{{$status->value}}" @selected(request('status')===$status->value)>{{$status->label()}}</option>@endforeach</select><select name="device" class="form-select"><option value="">{{__('serp_checks.device')}}</option>@foreach(\App\Enums\KeywordDevice::cases() as $device)<option value="{{$device->value}}" @selected(request('device')===$device->value)>{{$device->label()}}</option>@endforeach</select><input type="date" name="from" value="{{request('from')}}" class="form-control"><input type="date" name="to" value="{{request('to')}}" class="form-control"><button class="btn btn-primary">{{__('serp_checks.filter')}}</button><a class="btn btn-outline-secondary" href="{{route('serp-checks.index')}}">{{__('serp_checks.clear')}}</a></form></section>
|
||||
<section class="tp-panel"><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('serp_checks.keyword')}}</th><th>{{__('serp_checks.project')}}</th><th>{{__('serp_checks.type')}}</th><th>{{__('serp_checks.status')}}</th><th>{{__('serp_checks.rank')}}</th><th>{{__('serp_checks.device')}}</th><th>{{__('serp_checks.target')}}</th><th>{{__('serp_checks.requested')}}</th><th>{{__('serp_checks.completed_at')}}</th><th>{{__('serp_checks.action')}}</th></tr></thead><tbody>@forelse($checks as $check)<tr><td>{{$check->phrase}}</td><td>{{$check->keyword->project->name}}</td><td>{{__('serp_checks.'.($check->origin==='scheduled'?'scheduled':'manual'))}}</td><td><span class="badge tp-status-{{$check->status->value}}">{{$check->status->label()}}</span></td><td>{{$check->detected_rank?'#'.$check->detected_rank:($check->status===\App\Enums\SerpCheckStatus::Completed?'100+':'—')}}</td><td>{{$check->device->label()}}</td><td>{{$check->country_code}} / {{$check->language_code}}</td><td>{{$check->created_at->translatedFormat('d M Y H:i')}}</td><td>{{$check->completed_at?->translatedFormat('d M Y H:i')??'—'}}</td><td><a href="{{route('serp-checks.show',$check)}}">{{__('serp_checks.view')}}</a></td></tr>@empty<tr><td colspan="10"><x-empty-state :title="__('serp_checks.no_filter')" :description="__('serp_checks.no_checks')"/></td></tr>@endforelse</tbody></table></div>{{$checks->links()}}</section></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('serp_checks.details').' — TrafficPumper'"><x-customer-shell><nav><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{{route('serp-checks.index')}}">{{__('serp_checks.title')}}</a></li><li class="breadcrumb-item active">{{$check->phrase}}</li></ol></nav><header class="tp-page-header"><div><p>{{$check->keyword->project->name}} · {{$check->keyword->project->domain}}</p><h1>{{__('serp_checks.details')}}</h1><span>{{$check->status->label()}}</span></div></header><section class="tp-metric-grid tp-serp-metrics"><x-metric-card :label="__('serp_checks.rank')" :value="$rank?'#'.$rank:($check->status===\App\Enums\SerpCheckStatus::Completed?'100+':null)" icon="◎"/><x-metric-card :label="__('serp_checks.previous')" :value="$previous?'#'.$previous:null" icon="◷"/><x-metric-card :label="__('serp_checks.change')" :value="$change" icon="↕"/><x-metric-card :label="__('serp_checks.status')" :value="$check->status->label()" icon="●"/><x-metric-card :label="__('serp_checks.type')" :value="__('serp_checks.'.($check->origin==='scheduled'?'scheduled':'manual'))" icon="⌁"/><x-metric-card :label="__('serp_checks.completed_at')" :value="$check->completed_at?->diffForHumans()" icon="✓"/></section><div class="tp-two-columns"><section class="tp-panel"><dl class="tp-detail-list"><div><dt>{{__('serp_checks.keyword')}}</dt><dd>{{$check->phrase}}</dd></div><div><dt>{{__('serp_checks.engine')}}</dt><dd>{{$check->search_engine->label()}}</dd></div><div><dt>{{__('serp_checks.target')}}</dt><dd>{{$check->country_code}} / {{$check->language_code}} / {{$check->device->label()}}</dd></div><div><dt>{{__('serp_checks.requested')}}</dt><dd>{{$check->created_at->translatedFormat('d M Y H:i')}}</dd></div><div><dt>{{__('serp_checks.started')}}</dt><dd>{{$check->started_at?->translatedFormat('d M Y H:i')??'—'}}</dd></div><div><dt>{{__('serp_checks.found_url')}}</dt><dd>{{$check->results->firstWhere('is_project_domain',true)?->url??'—'}}</dd></div>@if($check->status===\App\Enums\SerpCheckStatus::Failed)<div><dt>{{__('serp_checks.safe_error')}}</dt><dd>{{__('serp_checks.error.'.(in_array($check->error_code,['validation','timeout'],true)?$check->error_code:'failed'))}}</dd></div>@endif</dl></section><section class="tp-panel"><div class="tp-panel-heading"><h2>{{__('serp_checks.organic')}}</h2></div><div class="table-responsive"><table class="table tp-data-table"><thead><tr><th>{{__('serp_checks.result_rank')}}</th><th>{{__('serp_checks.title_col')}}</th><th>{{__('serp_checks.url')}}</th><th>{{__('serp_checks.ours')}}</th></tr></thead><tbody>@forelse($check->results as $result)<tr><td>#{{$result->position}}</td><td>{{$result->title}}</td><td>@if(in_array(parse_url($result->url,PHP_URL_SCHEME),['http','https'],true))<a href="{{$result->url}}" rel="noopener noreferrer">{{$result->display_url?:parse_url($result->url,PHP_URL_HOST)}}</a>@else{{$result->display_url}}@endif</td><td>{{$result->is_project_domain?__('serp_checks.yes'):__('serp_checks.no')}}</td></tr>@empty<tr><td colspan="4">{{__('serp_checks.no_checks')}}</td></tr>@endforelse</tbody></table></div></section></div></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="'SERP Geçmişi — '.$keyword->phrase"><div class="container-fluid"><div class="row"><x-customer-sidebar/><main class="col-lg-10 p-4 p-lg-5"><div class="d-flex flex-wrap justify-content-between gap-3 mb-4"><div><h1 class="h2">SERP kontrol geçmişi</h1><a href="{{ route('projects.keywords.show',[$project,$keyword]) }}">{{ $keyword->phrase }}</a></div>@if($keyword->status===\App\Enums\KeywordStatus::Active)@if(config('serp.enabled'))<form method="POST" action="{{ route('projects.keywords.serp-checks.store',[$project,$keyword]) }}">@csrf<button class="btn btn-primary">Kontrol başlat</button></form>@else<span class="alert alert-secondary py-2 mb-0">SERP kontrol hizmeti henüz yapılandırılmamış.</span>@endif @endif</div>@error('serp')<div class="alert alert-warning">{{ $message }}</div>@enderror<div class="card border-0 shadow-sm"><div class="table-responsive"><table class="table mb-0"><thead><tr><th>Zaman</th><th>Hedef</th><th>Durum</th><th>Sonuç</th></tr></thead><tbody>@forelse($checks as $check)<tr><td><a href="{{ route('projects.keywords.serp-checks.show',[$project,$keyword,$check]) }}">{{ $check->created_at->format('d.m.Y H:i') }}</a></td><td>{{ $check->search_engine->label() }} · {{ $check->country_code }}/{{ $check->language_code }} · {{ $check->device->label() }}</td><td>{{ $check->status->label() }}</td><td>{{ $check->status===\App\Enums\SerpCheckStatus::Completed?$check->result_count:'—' }}</td></tr>@empty<tr><td colspan="4" class="text-center py-5 text-body-secondary">Henüz gerçek kontrol kaydı yok.</td></tr>@endforelse</tbody></table></div></div><div class="mt-4">{{ $checks->links() }}</div></main></div></div></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app title="SERP Kontrolü — TrafficPumper"><div class="container-fluid"><div class="row"><x-customer-sidebar/><main class="col-lg-10 p-4 p-lg-5"><h1 class="h2">SERP kontrolü</h1><p>{{ $check->phrase }} · {{ $check->status->label() }} · En iyi proje pozisyonu: {{ $best??'—' }}</p><div class="card border-0 shadow-sm"><div class="table-responsive"><table class="table mb-0"><thead><tr><th>Pozisyon</th><th>Sonuç</th><th>Proje</th></tr></thead><tbody>@forelse($check->results as $result)<tr><td>{{ $result->position }}</td><td><a href="{{ $result->url }}" rel="noopener noreferrer">{{ $result->title?:$result->display_url }}</a>@if($result->snippet)<div class="text-body-secondary">{{ $result->snippet }}</div>@endif</td><td>{{ $result->is_project_domain?'Evet':'Hayır' }}</td></tr>@empty<tr><td colspan="3" class="text-center py-5 text-body-secondary">Kaydedilmiş sonuç yok.</td></tr>@endforelse</tbody></table></div></div></main></div></div></x-layouts.app>
|
||||
@@ -0,0 +1,7 @@
|
||||
<x-layouts.app :title="__('settings.title')"><x-customer-shell><header class="tp-page-header"><div><p>{{__('customer.my_account')}}</p><h1>{{__('settings.title')}}</h1><span>{{__('settings.description')}}</span></div></header><div class="tp-settings-grid">
|
||||
<section class="tp-panel"><h2 class="h5">{{__('settings.appearance')}}</h2><form method="post" action="{{route('settings.appearance')}}">@csrf @method('PUT')<label class="form-label">{{__('settings.language')}}<select class="form-select" name="locale"><option value="tr" @selected(auth()->user()->locale==='tr')>Türkçe</option><option value="en" @selected(auth()->user()->locale==='en')>English</option></select></label><label class="form-label">{{__('settings.theme')}}<select class="form-select" name="theme" data-settings-theme>@foreach(['light','dark','system'] as $v)<option value="{{$v}}" @selected($setting->theme===$v)>{{__('settings.'.$v)}}</option>@endforeach</select></label><label class="form-check"><input type="hidden" name="reduced_motion" value="0"><input class="form-check-input" type="checkbox" name="reduced_motion" value="1" @checked($setting->reduced_motion)> {{__('settings.reduced_motion')}}</label><button class="btn btn-primary mt-3">{{__('settings.save')}}</button></form></section>
|
||||
<section class="tp-panel"><h2 class="h5">{{__('settings.regional')}}</h2><form method="post" action="{{route('settings.regional')}}">@csrf @method('PUT')<label class="form-label">{{__('settings.timezone')}}<select class="form-select" name="timezone">@foreach(DateTimeZone::listIdentifiers() as $zone)<option @selected($setting->timezone===$zone)>{{$zone}}</option>@endforeach</select></label><label class="form-label">{{__('settings.date_format')}}<select class="form-select" name="date_format">@foreach(['DD.MM.YYYY','YYYY-MM-DD','MM/DD/YYYY'] as $v)<option @selected($setting->date_format===$v)>{{$v}}</option>@endforeach</select></label><label class="form-label">{{__('settings.time_format')}}<select class="form-select" name="time_format"><option value="24" @selected($setting->time_format==='24')>24</option><option value="12" @selected($setting->time_format==='12')>12</option></select></label><button class="btn btn-primary">{{__('settings.save')}}</button></form></section>
|
||||
<section class="tp-panel"><h2 class="h5">{{__('settings.dashboard')}}</h2><form method="post" action="{{route('settings.dashboard')}}">@csrf @method('PUT')<label class="form-label">{{__('settings.default_project')}}<select class="form-select" name="default_project"><option value="">{{__('settings.all_projects')}}</option>@foreach($projects as $p)<option value="{{$p->ulid}}" @selected($setting->default_project_id===$p->id)>{{$p->name}}</option>@endforeach</select></label><label class="form-label">{{__('settings.default_range')}}<select class="form-select" name="default_report_range">@foreach(['7_days','30_days','90_days','this_month'] as $v)<option value="{{$v}}" @selected($setting->default_report_range===$v)>{{__('settings.ranges.'.$v)}}</option>@endforeach</select></label><label class="form-label">{{__('settings.per_page')}}<select class="form-select" name="per_page">@foreach([10,25,50,100] as $v)<option @selected($setting->per_page===$v)>{{$v}}</option>@endforeach</select></label><button class="btn btn-primary">{{__('settings.save')}}</button></form></section>
|
||||
<section class="tp-panel"><h2 class="h5">{{__('settings.notifications')}}</h2><form method="post" action="{{route('settings.notifications')}}">@csrf @method('PUT')<label class="form-check"><input type="hidden" name="in_app_notifications" value="0"><input class="form-check-input" type="checkbox" name="in_app_notifications" value="1" @checked($setting->in_app_notifications)> {{__('settings.in_app')}}</label><button class="btn btn-primary mt-3">{{__('settings.save')}}</button></form></section>
|
||||
<section class="tp-panel tp-settings-reset"><h2 class="h5">{{__('settings.reset')}}</h2><p>{{__('settings.reset_help')}}</p><form method="post" action="{{route('settings.reset')}}" onsubmit="return confirm('{{__('settings.reset')}}?')">@csrf<button class="btn btn-outline-danger">{{__('settings.reset')}}</button></form></section>
|
||||
</div></x-customer-shell></x-layouts.app>
|
||||
@@ -0,0 +1,54 @@
|
||||
@php($portal = app(\App\Support\PortalNavigationContext::class)->resolve(request()))
|
||||
<x-layouts.app :title="__('landing.meta_title')">
|
||||
<main class="tp-landing">
|
||||
<section class="tp-landing-hero">
|
||||
<div class="container">
|
||||
<div class="tp-landing-hero-copy">
|
||||
<span class="tp-landing-eyebrow">{{ __('landing.eyebrow') }}</span>
|
||||
<h1>{{ __('landing.hero_title') }}</h1>
|
||||
<p>{{ __('landing.hero_description') }}</p>
|
||||
<div class="tp-landing-actions">
|
||||
@if($portal['kind'] === 'web')
|
||||
<a class="btn btn-primary btn-lg" href="{{ $portal['panel_url'] }}">{{ $portal['panel_label'] }}</a>
|
||||
@else
|
||||
<a class="btn btn-primary btn-lg" href="{{ route('register') }}">{{ __('landing.create_account') }}</a>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="{{ route('login') }}">{{ __('ui.sign_in') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="tp-landing-preview" aria-label="{{ __('landing.preview_label') }}">
|
||||
<div class="tp-preview-heading"><span>{{ __('landing.preview_project') }}</span><strong>{{ __('landing.preview_visibility') }}</strong></div>
|
||||
<div class="tp-preview-metric"><span>{{ __('landing.preview_keywords') }}</span><strong>128</strong></div>
|
||||
<div class="tp-preview-chart" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
||||
<div class="tp-preview-status"><span>● {{ __('landing.preview_gained') }}</span><span>● {{ __('landing.preview_stable') }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tp-landing-section" aria-labelledby="features-title">
|
||||
<div class="container">
|
||||
<div class="tp-section-heading"><span>{{ __('landing.features_eyebrow') }}</span><h2 id="features-title">{{ __('landing.features_title') }}</h2><p>{{ __('landing.features_description') }}</p></div>
|
||||
<div class="tp-feature-grid">
|
||||
@foreach(['keywords' => '◎', 'history' => '◷', 'serp' => '⌕', 'visibility' => '◒', 'movers' => '↕', 'competitors' => '◇', 'projects' => '▦', 'preferences' => '◐'] as $feature => $icon)
|
||||
<article class="tp-feature-card"><span class="tp-feature-icon" aria-hidden="true">{{ $icon }}</span><h3>{{ __('landing.features.'.$feature.'.title') }}</h3><p>{{ __('landing.features.'.$feature.'.description') }}</p></article>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tp-landing-section tp-how-section" aria-labelledby="how-title">
|
||||
<div class="container">
|
||||
<div class="tp-section-heading"><span>{{ __('landing.how_eyebrow') }}</span><h2 id="how-title">{{ __('landing.how_title') }}</h2></div>
|
||||
<ol class="tp-how-grid">
|
||||
@foreach(['project', 'keywords', 'track'] as $step)
|
||||
<li><span>{{ $loop->iteration }}</span><div><h3>{{ __('landing.steps.'.$step.'.title') }}</h3><p>{{ __('landing.steps.'.$step.'.description') }}</p></div></li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tp-landing-cta"><div class="container"><div><h2>{{ __('landing.cta_title') }}</h2><p>{{ __('landing.cta_description') }}</p></div>@if($portal['kind'] === 'web')<a class="btn btn-primary btn-lg" href="{{ $portal['panel_url'] }}">{{ $portal['panel_label'] }}</a>@else<a class="btn btn-primary btn-lg" href="{{ route('register') }}">{{ __('landing.create_account') }}</a>@endif</div></section>
|
||||
</main>
|
||||
|
||||
<footer class="tp-landing-footer"><div class="container"><a href="{{ route('home') }}" class="tp-footer-brand">TrafficPumper</a><nav aria-label="{{ __('landing.footer_navigation') }}"><a href="{{ route('earn') }}">{{ __('earn.navigation') }}</a>@if($portal['kind'] === 'web')<a href="{{ $portal['panel_url'] }}">{{ $portal['panel_label'] }}</a>@else<a href="{{ route('login') }}">{{ __('ui.sign_in') }}</a><a href="{{ route('register') }}">{{ __('ui.register') }}</a>@endif</nav><small>© {{ now()->year }} TrafficPumper</small></div></footer>
|
||||
</x-layouts.app>
|
||||
Reference in New Issue
Block a user